From f561120295d392678a19d5a8a578da272a082bc0 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Sat, 7 Jan 2023 23:40:02 +0100 Subject: [PATCH 0001/2100] Remove info labels, since they are no longer present in this component in thew new design --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 356 ++++++++++++++++++ 1 file changed, 356 insertions(+) create mode 100644 osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs new file mode 100644 index 0000000000..4d1a2133fd --- /dev/null +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -0,0 +1,356 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using System.Collections.Generic; +using System.Threading; +using osuTK; +using osuTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Effects; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Screens.Select +{ + public partial class BeatmapInfoWedgeV2 : VisibilityContainer + { + public const float BORDER_THICKNESS = 2.5f; + private const float shear_width = 36.75f; + + private const float transition_duration = 250; + + private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0); + + [Resolved] + private IBindable ruleset { get; set; } + + protected Container DisplayedContent { get; private set; } + + protected WedgeInfoText Info { get; private set; } + + public BeatmapInfoWedgeV2() + { + Shear = wedged_container_shear; + Masking = true; + BorderColour = new Color4(221, 255, 255, 255); + BorderThickness = BORDER_THICKNESS; + Alpha = 0; + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = new Color4(130, 204, 255, 150), + Radius = 20, + Roundness = 15, + }; + } + + [BackgroundDependencyLoader] + private void load() + { + ruleset.BindValueChanged(_ => updateDisplay()); + } + + private const double animation_duration = 800; + + protected override void PopIn() + { + this.MoveToX(0, animation_duration, Easing.OutQuint); + this.RotateTo(0, animation_duration, Easing.OutQuint); + this.FadeIn(transition_duration); + } + + protected override void PopOut() + { + this.MoveToX(-100, animation_duration, Easing.In); + this.RotateTo(10, animation_duration, Easing.In); + this.FadeOut(transition_duration * 2, Easing.In); + } + + private WorkingBeatmap beatmap; + + public WorkingBeatmap Beatmap + { + get => beatmap; + set + { + if (beatmap == value) return; + + beatmap = value; + + updateDisplay(); + } + } + + public override bool IsPresent => base.IsPresent || DisplayedContent == null; // Visibility is updated in the LoadComponentAsync callback + + private Container loadingInfo; + + private void updateDisplay() + { + Scheduler.AddOnce(perform); + + void perform() + { + void removeOldInfo() + { + State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible; + + DisplayedContent?.FadeOut(transition_duration); + DisplayedContent?.Expire(); + DisplayedContent = null; + } + + if (beatmap == null) + { + removeOldInfo(); + return; + } + + LoadComponentAsync(loadingInfo = new Container + { + RelativeSizeAxes = Axes.Both, + Shear = -Shear, + Depth = DisplayedContent?.Depth + 1 ?? 0, + Children = new Drawable[] + { + new BeatmapInfoWedgeBackground(beatmap), + Info = new WedgeInfoText(beatmap), + } + }, loaded => + { + // ensure we are the most recent loaded wedge. + if (loaded != loadingInfo) return; + + removeOldInfo(); + Add(DisplayedContent = loaded); + }); + } + } + + public partial class WedgeInfoText : Container + { + public OsuSpriteText VersionLabel { get; private set; } + public OsuSpriteText TitleLabel { get; private set; } + public OsuSpriteText ArtistLabel { get; private set; } + public FillFlowContainer MapperContainer { get; private set; } + + private Container difficultyColourBar; + private StarRatingDisplay starRatingDisplay; + + private ILocalisedBindableString titleBinding; + private ILocalisedBindableString artistBinding; + + private readonly WorkingBeatmap working; + + [Resolved] + private IBindable> mods { get; set; } + + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } + + [Resolved] + private OsuColour colours { get; set; } + + private ModSettingChangeTracker settingChangeTracker; + + public WedgeInfoText(WorkingBeatmap working) + { + this.working = working; + } + + private CancellationTokenSource cancellationSource; + private IBindable starDifficulty; + + [BackgroundDependencyLoader] + private void load(LocalisationManager localisation) + { + var beatmapInfo = working.BeatmapInfo; + var metadata = beatmapInfo.Metadata; + + RelativeSizeAxes = Axes.Both; + + titleBinding = localisation.GetLocalisedBindableString(new RomanisableString(metadata.TitleUnicode, metadata.Title)); + artistBinding = localisation.GetLocalisedBindableString(new RomanisableString(metadata.ArtistUnicode, metadata.Artist)); + + const float top_height = 0.7f; + + Children = new Drawable[] + { + difficultyColourBar = new Container + { + RelativeSizeAxes = Axes.Y, + Width = 20f, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Width = top_height, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Alpha = 0.5f, + X = top_height, + Width = 1 - top_height, + } + } + }, + new FillFlowContainer + { + Name = "Topleft-aligned metadata", + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 10, Left = 25, Right = shear_width * 2.5f }, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Children = new Drawable[] + { + VersionLabel = new OsuSpriteText + { + Text = beatmapInfo.DifficultyName, + Font = OsuFont.GetFont(size: 24, italics: true), + RelativeSizeAxes = Axes.X, + Truncate = true, + }, + } + }, + new FillFlowContainer + { + Name = "Topright-aligned metadata", + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 14, Right = shear_width / 2 }, + AutoSizeAxes = Axes.Both, + Shear = wedged_container_shear, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] + { + starRatingDisplay = new StarRatingDisplay(default, animated: true) + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Shear = -wedged_container_shear, + Alpha = 0f, + }, + new BeatmapSetOnlineStatusPill + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Shear = -wedged_container_shear, + TextSize = 11, + TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + Status = beatmapInfo.Status, + Alpha = string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? 0 : 1 + } + } + }, + new FillFlowContainer + { + Name = "Centre-aligned metadata", + Anchor = Anchor.CentreLeft, + Origin = Anchor.TopLeft, + Y = -7, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Left = 25, Right = shear_width }, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Children = new Drawable[] + { + TitleLabel = new OsuSpriteText + { + Current = { BindTarget = titleBinding }, + Font = OsuFont.GetFont(size: 28, italics: true), + RelativeSizeAxes = Axes.X, + Truncate = true, + }, + ArtistLabel = new OsuSpriteText + { + Current = { BindTarget = artistBinding }, + Font = OsuFont.GetFont(size: 17, italics: true), + RelativeSizeAxes = Axes.X, + Truncate = true, + }, + MapperContainer = new FillFlowContainer + { + Margin = new MarginPadding { Top = 10 }, + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Child = getMapper(metadata), + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + starRatingDisplay.DisplayedStars.BindValueChanged(s => + { + difficultyColourBar.Colour = colours.ForStarDifficulty(s.NewValue); + }, true); + + starDifficulty = difficultyCache.GetBindableDifficulty(working.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token); + starDifficulty.BindValueChanged(s => + { + starRatingDisplay.Current.Value = s.NewValue ?? default; + + // Don't roll the counter on initial display (but still allow it to roll on applying mods etc.) + if (!starRatingDisplay.IsPresent) + starRatingDisplay.FinishTransforms(true); + + starRatingDisplay.FadeIn(transition_duration); + }); + + mods.BindValueChanged(m => + { + settingChangeTracker?.Dispose(); + + settingChangeTracker = new ModSettingChangeTracker(m.NewValue); + }, true); + } + + private Drawable getMapper(BeatmapMetadata metadata) + { + if (string.IsNullOrEmpty(metadata.Author.Username)) + return Empty(); + + return new LinkFlowContainer(s => + { + s.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 15); + }).With(d => + { + d.AutoSizeAxes = Axes.Both; + d.AddText("mapped by "); + d.AddUserLink(metadata.Author); + }); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + settingChangeTracker?.Dispose(); + cancellationSource?.Cancel(); + } + } + } +} From c646f8479b35cc7d66b1075ad5c5ba8ab46434ad Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Sat, 7 Jan 2023 23:53:19 +0100 Subject: [PATCH 0002/2100] Move stardifficulty logic to main BeatmapInfoWedge class instead of text subclass ( for use by background star rating colour bar ) --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index 4d1a2133fd..104fa8787b 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -37,10 +37,16 @@ namespace osu.Game.Screens.Select [Resolved] private IBindable ruleset { get; set; } + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } + protected Container DisplayedContent { get; private set; } protected WedgeInfoText Info { get; private set; } + private IBindable starDifficulty; + private CancellationTokenSource cancellationSource; + public BeatmapInfoWedgeV2() { Shear = wedged_container_shear; @@ -98,6 +104,19 @@ namespace osu.Game.Screens.Select private Container loadingInfo; + protected override void LoadComplete() + { + base.LoadComplete(); + starDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + cancellationSource?.Cancel(); + } + private void updateDisplay() { Scheduler.AddOnce(perform); @@ -127,7 +146,7 @@ namespace osu.Game.Screens.Select Children = new Drawable[] { new BeatmapInfoWedgeBackground(beatmap), - Info = new WedgeInfoText(beatmap), + Info = new WedgeInfoText(beatmap, starDifficulty), } }, loaded => { @@ -154,26 +173,22 @@ namespace osu.Game.Screens.Select private ILocalisedBindableString artistBinding; private readonly WorkingBeatmap working; + private readonly IBindable starDifficulty; [Resolved] private IBindable> mods { get; set; } - [Resolved] - private BeatmapDifficultyCache difficultyCache { get; set; } - [Resolved] private OsuColour colours { get; set; } private ModSettingChangeTracker settingChangeTracker; - public WedgeInfoText(WorkingBeatmap working) + public WedgeInfoText(WorkingBeatmap working, IBindable starDifficulty) { this.working = working; + this.starDifficulty = starDifficulty; } - private CancellationTokenSource cancellationSource; - private IBindable starDifficulty; - [BackgroundDependencyLoader] private void load(LocalisationManager localisation) { @@ -309,7 +324,6 @@ namespace osu.Game.Screens.Select difficultyColourBar.Colour = colours.ForStarDifficulty(s.NewValue); }, true); - starDifficulty = difficultyCache.GetBindableDifficulty(working.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token); starDifficulty.BindValueChanged(s => { starRatingDisplay.Current.Value = s.NewValue ?? default; @@ -349,7 +363,6 @@ namespace osu.Game.Screens.Select { base.Dispose(isDisposing); settingChangeTracker?.Dispose(); - cancellationSource?.Cancel(); } } } From 0199c19f74705c2d5c0dd10f3c61c0c8325ef224 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Sun, 8 Jan 2023 01:24:47 +0100 Subject: [PATCH 0003/2100] Add a test scene and move colour bar to back and adjust positioning of it --- .../SongSelect/TestSceneBeatmapInfoWedgeV2.cs | 187 ++++++++++++++++++ osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 131 +++--------- 2 files changed, 217 insertions(+), 101 deletions(-) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs new file mode 100644 index 0000000000..98e9d803ca --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs @@ -0,0 +1,187 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using System.Collections.Generic; +using JetBrains.Annotations; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Screens.Select; +using osuTK; + +namespace osu.Game.Tests.Visual.SongSelect +{ + [TestFixture] + public partial class TestSceneBeatmapInfoWedgeV2 : OsuTestScene + { + private RulesetStore rulesets; + private TestBeatmapInfoWedgeV2 infoWedge; + private readonly List beatmaps = new List(); + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + this.rulesets = rulesets; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Add(infoWedge = new TestBeatmapInfoWedgeV2 + { + Size = new Vector2(0.6f, 120), + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Top = 20 } + }); + + AddStep("show", () => infoWedge.Show()); + + selectBeatmap(Beatmap.Value.Beatmap); + + AddWaitStep("wait for select", 3); + + AddStep("hide", () => { infoWedge.Hide(); }); + + AddWaitStep("wait for hide", 3); + + AddStep("show", () => { infoWedge.Show(); }); + + AddSliderStep("change star difficulty", 0, 11.9, 5.55, v => + { + foreach (var hasCurrentValue in infoWedge.Info.ChildrenOfType>()) + hasCurrentValue.Current.Value = new StarDifficulty(v, 0); + }); + + foreach (var rulesetInfo in rulesets.AvailableRulesets) + { + var instance = rulesetInfo.CreateInstance(); + var testBeatmap = createTestBeatmap(rulesetInfo); + + beatmaps.Add(testBeatmap); + + setRuleset(rulesetInfo); + + selectBeatmap(testBeatmap); + + testBeatmapLabels(instance); + } + } + + private void testBeatmapLabels(Ruleset ruleset) + { + AddAssert("check title", () => infoWedge.Info.TitleLabel.Current.Value == $"{ruleset.ShortName}Title"); + AddAssert("check artist", () => infoWedge.Info.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist"); + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("reset mods", () => SelectedMods.SetDefault()); + } + + [Test] + public void TestTruncation() + { + selectBeatmap(createLongMetadata()); + } + + private void setRuleset(RulesetInfo rulesetInfo) + { + Container containerBefore = null; + + AddStep("set ruleset", () => + { + // wedge content is only refreshed if the ruleset changes, so only wait for load in that case. + if (!rulesetInfo.Equals(Ruleset.Value)) + containerBefore = infoWedge.DisplayedContent; + + Ruleset.Value = rulesetInfo; + }); + + AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); + } + + private void selectBeatmap([CanBeNull] IBeatmap b) + { + Container containerBefore = null; + + AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () => + { + containerBefore = infoWedge.DisplayedContent; + infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b); + }); + + AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); + } + + private IBeatmap createTestBeatmap(RulesetInfo ruleset) + { + List objects = new List(); + for (double i = 0; i < 50000; i += 1000) + objects.Add(new TestHitObject { StartTime = i }); + + return new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Author = { Username = $"{ruleset.ShortName}Author" }, + Artist = $"{ruleset.ShortName}Artist", + Source = $"{ruleset.ShortName}Source", + Title = $"{ruleset.ShortName}Title" + }, + Ruleset = ruleset, + StarRating = 6, + DifficultyName = $"{ruleset.ShortName}Version", + Difficulty = new BeatmapDifficulty() + }, + HitObjects = objects + }; + } + + private IBeatmap createLongMetadata() + { + return new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Author = { Username = "WWWWWWWWWWWWWWW" }, + Artist = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Artist", + Source = "Verrrrry long Source", + Title = "Verrrrry long Title" + }, + DifficultyName = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Version", + Status = BeatmapOnlineStatus.Graveyard, + }, + }; + } + + private partial class TestBeatmapInfoWedgeV2 : BeatmapInfoWedgeV2 + { + public new Container DisplayedContent => base.DisplayedContent; + + public new WedgeInfoText Info => base.Info; + } + + private class TestHitObject : ConvertHitObject, IHasPosition + { + public float X => 0; + public float Y => 0; + public Vector2 Position { get; } = Vector2.Zero; + } + } +} diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index 104fa8787b..5583ad11f7 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Threading; using osuTK; -using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -16,18 +15,16 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Effects; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Graphics.Containers; namespace osu.Game.Screens.Select { + [Cached] public partial class BeatmapInfoWedgeV2 : VisibilityContainer { - public const float BORDER_THICKNESS = 2.5f; private const float shear_width = 36.75f; private const float transition_duration = 250; @@ -44,22 +41,25 @@ namespace osu.Game.Screens.Select protected WedgeInfoText Info { get; private set; } - private IBindable starDifficulty; + private IBindable starDifficulty = new Bindable(); private CancellationTokenSource cancellationSource; + private readonly Container difficultyColourBar; + public BeatmapInfoWedgeV2() { + CornerRadius = 10; Shear = wedged_container_shear; Masking = true; - BorderColour = new Color4(221, 255, 255, 255); - BorderThickness = BORDER_THICKNESS; Alpha = 0; - EdgeEffect = new EdgeEffectParameters + Child = difficultyColourBar = new Container { - Type = EdgeEffectType.Glow, - Colour = new Color4(130, 204, 255, 150), - Radius = 20, - Roundness = 15, + Depth = float.MaxValue, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Y, + Width = 40, + Child = new Box { RelativeSizeAxes = Axes.Both } }; } @@ -95,6 +95,7 @@ namespace osu.Game.Screens.Select if (beatmap == value) return; beatmap = value; + starDifficulty = difficultyCache.GetBindableDifficulty(value.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token); updateDisplay(); } @@ -104,12 +105,6 @@ namespace osu.Game.Screens.Select private Container loadingInfo; - protected override void LoadComplete() - { - base.LoadComplete(); - starDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token); - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -140,13 +135,15 @@ namespace osu.Game.Screens.Select LoadComponentAsync(loadingInfo = new Container { + Masking = true, + X = -30, + CornerRadius = 10, RelativeSizeAxes = Axes.Both, - Shear = -Shear, Depth = DisplayedContent?.Depth + 1 ?? 0, Children = new Drawable[] { - new BeatmapInfoWedgeBackground(beatmap), - Info = new WedgeInfoText(beatmap, starDifficulty), + new BeatmapInfoWedgeBackground(beatmap) { Shear = -Shear }, + Info = new WedgeInfoText(beatmap, starDifficulty) { Shear = -Shear } } }, loaded => { @@ -161,12 +158,9 @@ namespace osu.Game.Screens.Select public partial class WedgeInfoText : Container { - public OsuSpriteText VersionLabel { get; private set; } public OsuSpriteText TitleLabel { get; private set; } public OsuSpriteText ArtistLabel { get; private set; } - public FillFlowContainer MapperContainer { get; private set; } - private Container difficultyColourBar; private StarRatingDisplay starRatingDisplay; private ILocalisedBindableString titleBinding; @@ -178,6 +172,9 @@ namespace osu.Game.Screens.Select [Resolved] private IBindable> mods { get; set; } + [Resolved] + private BeatmapInfoWedgeV2 wedge { get; set; } + [Resolved] private OsuColour colours { get; set; } @@ -200,51 +197,8 @@ namespace osu.Game.Screens.Select titleBinding = localisation.GetLocalisedBindableString(new RomanisableString(metadata.TitleUnicode, metadata.Title)); artistBinding = localisation.GetLocalisedBindableString(new RomanisableString(metadata.ArtistUnicode, metadata.Artist)); - const float top_height = 0.7f; - Children = new Drawable[] { - difficultyColourBar = new Container - { - RelativeSizeAxes = Axes.Y, - Width = 20f, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Width = top_height, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Both, - Alpha = 0.5f, - X = top_height, - Width = 1 - top_height, - } - } - }, - new FillFlowContainer - { - Name = "Topleft-aligned metadata", - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Top = 10, Left = 25, Right = shear_width * 2.5f }, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Children = new Drawable[] - { - VersionLabel = new OsuSpriteText - { - Text = beatmapInfo.DifficultyName, - Font = OsuFont.GetFont(size: 24, italics: true), - RelativeSizeAxes = Axes.X, - Truncate = true, - }, - } - }, new FillFlowContainer { Name = "Topright-aligned metadata", @@ -279,12 +233,10 @@ namespace osu.Game.Screens.Select }, new FillFlowContainer { - Name = "Centre-aligned metadata", - Anchor = Anchor.CentreLeft, - Origin = Anchor.TopLeft, - Y = -7, + Name = "Top-left aligned metadata", Direction = FillDirection.Vertical, - Padding = new MarginPadding { Left = 25, Right = shear_width }, + Position = new Vector2(50, 12), + Width = .8f, AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Children = new Drawable[] @@ -292,23 +244,17 @@ namespace osu.Game.Screens.Select TitleLabel = new OsuSpriteText { Current = { BindTarget = titleBinding }, - Font = OsuFont.GetFont(size: 28, italics: true), + Font = OsuFont.TorusAlternate.With(size: 40, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Truncate = true, + Truncate = true }, ArtistLabel = new OsuSpriteText { Current = { BindTarget = artistBinding }, - Font = OsuFont.GetFont(size: 17, italics: true), + //Not sure if this should be semi bold or medium + Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Truncate = true, - }, - MapperContainer = new FillFlowContainer - { - Margin = new MarginPadding { Top = 10 }, - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Child = getMapper(metadata), + Truncate = true } } } @@ -321,9 +267,8 @@ namespace osu.Game.Screens.Select starRatingDisplay.DisplayedStars.BindValueChanged(s => { - difficultyColourBar.Colour = colours.ForStarDifficulty(s.NewValue); + wedge.difficultyColourBar.FadeColour(colours.ForStarDifficulty(s.NewValue)); }, true); - starDifficulty.BindValueChanged(s => { starRatingDisplay.Current.Value = s.NewValue ?? default; @@ -343,22 +288,6 @@ namespace osu.Game.Screens.Select }, true); } - private Drawable getMapper(BeatmapMetadata metadata) - { - if (string.IsNullOrEmpty(metadata.Author.Username)) - return Empty(); - - return new LinkFlowContainer(s => - { - s.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 15); - }).With(d => - { - d.AutoSizeAxes = Axes.Both; - d.AddText("mapped by "); - d.AddUserLink(metadata.Author); - }); - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 61caabaa8ee4002c7d96859f0937b3fb8b997c82 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Sun, 8 Jan 2023 01:47:22 +0100 Subject: [PATCH 0004/2100] Add coloured star counter --- .../Graphics/UserInterface/StarCounter.cs | 6 ++- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 51 +++++++++++++++---- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/StarCounter.cs b/osu.Game/Graphics/UserInterface/StarCounter.cs index d7d088d798..7adb482188 100644 --- a/osu.Game/Graphics/UserInterface/StarCounter.cs +++ b/osu.Game/Graphics/UserInterface/StarCounter.cs @@ -32,6 +32,11 @@ namespace osu.Game.Graphics.UserInterface private const float star_spacing = 4; + public virtual FillDirection Direction + { + set => stars.Direction = value; + } + private float current; /// @@ -66,7 +71,6 @@ namespace osu.Game.Graphics.UserInterface stars = new FillFlowContainer { AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, Spacing = new Vector2(star_spacing), ChildrenEnumerable = Enumerable.Range(0, StarCount).Select(_ => CreateStar()) } diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index 5583ad11f7..07fcb42fff 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -3,7 +3,9 @@ #nullable disable +using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using osuTK; using osu.Framework.Allocation; @@ -17,6 +19,7 @@ using osu.Game.Graphics.Sprites; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -25,11 +28,12 @@ namespace osu.Game.Screens.Select [Cached] public partial class BeatmapInfoWedgeV2 : VisibilityContainer { - private const float shear_width = 36.75f; + private const float shear_width = 21; + private const int wedge_height = 120; private const float transition_duration = 250; - private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0); + private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / wedge_height, 0); [Resolved] private IBindable ruleset { get; set; } @@ -45,6 +49,7 @@ namespace osu.Game.Screens.Select private CancellationTokenSource cancellationSource; private readonly Container difficultyColourBar; + private readonly StarCounter starCounter; public BeatmapInfoWedgeV2() { @@ -52,14 +57,27 @@ namespace osu.Game.Screens.Select Shear = wedged_container_shear; Masking = true; Alpha = 0; - Child = difficultyColourBar = new Container + + Children = new Drawable[] { - Depth = float.MaxValue, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Y, - Width = 40, - Child = new Box { RelativeSizeAxes = Axes.Both } + difficultyColourBar = new Container + { + Depth = float.MaxValue, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Y, + Width = 40, + Child = new Box { RelativeSizeAxes = Axes.Both } + }, + starCounter = new StarCounter + { + Anchor = Anchor.CentreRight, + Origin = Anchor.Centre, + Scale = new Vector2(0.4f), + Shear = -wedged_container_shear, + X = -15, + Direction = FillDirection.Vertical + } }; } @@ -67,6 +85,15 @@ namespace osu.Game.Screens.Select private void load() { ruleset.BindValueChanged(_ => updateDisplay()); + + float starAngle = (float)(Math.Atan(shear_width / wedge_height) * (180 / Math.PI)); + + //Applying the rotation directly to the StarCounter distorts the stars, hence it is applied to the child container + starCounter.Children.First().Rotation = starAngle; + + //Makes sure the stars center themselves properly in the colour bar + starCounter.Children.First().Anchor = Anchor.Centre; + starCounter.Children.First().Origin = Anchor.Centre; } private const double animation_duration = 800; @@ -267,8 +294,12 @@ namespace osu.Game.Screens.Select starRatingDisplay.DisplayedStars.BindValueChanged(s => { - wedge.difficultyColourBar.FadeColour(colours.ForStarDifficulty(s.NewValue)); + wedge.starCounter.Colour = s.NewValue >= 6.5 ? colours.Orange1 : Colour4.Black.Opacity(0.75f); + wedge.starCounter.Current = (float)s.NewValue; + + wedge.difficultyColourBar.FadeColour(colours.ForStarDifficulty(s.NewValue), 750, Easing.OutQuint); }, true); + starDifficulty.BindValueChanged(s => { starRatingDisplay.Current.Value = s.NewValue ?? default; From 1698272eb88b3937228783d3df8d04db993d5da5 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Sun, 8 Jan 2023 12:03:02 +0100 Subject: [PATCH 0005/2100] Simplify passing data from BeatmapInfoWedgeV2.cs to subclass wedgeinfotext --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index 07fcb42fff..1056d9478b 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -60,17 +60,22 @@ namespace osu.Game.Screens.Select Children = new Drawable[] { + //These elements can't be grouped with the rest of the content, due to being present either outside or under the backgrounds area difficultyColourBar = new Container { + Colour = Colour4.Transparent, Depth = float.MaxValue, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Y, + + //By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it. Width = 40, Child = new Box { RelativeSizeAxes = Axes.Both } }, starCounter = new StarCounter { + Colour = Colour4.Transparent, Anchor = Anchor.CentreRight, Origin = Anchor.Centre, Scale = new Vector2(0.4f), @@ -170,7 +175,7 @@ namespace osu.Game.Screens.Select Children = new Drawable[] { new BeatmapInfoWedgeBackground(beatmap) { Shear = -Shear }, - Info = new WedgeInfoText(beatmap, starDifficulty) { Shear = -Shear } + Info = new WedgeInfoText { Shear = -Shear } } }, loaded => { @@ -193,9 +198,6 @@ namespace osu.Game.Screens.Select private ILocalisedBindableString titleBinding; private ILocalisedBindableString artistBinding; - private readonly WorkingBeatmap working; - private readonly IBindable starDifficulty; - [Resolved] private IBindable> mods { get; set; } @@ -207,17 +209,11 @@ namespace osu.Game.Screens.Select private ModSettingChangeTracker settingChangeTracker; - public WedgeInfoText(WorkingBeatmap working, IBindable starDifficulty) - { - this.working = working; - this.starDifficulty = starDifficulty; - } - [BackgroundDependencyLoader] private void load(LocalisationManager localisation) { - var beatmapInfo = working.BeatmapInfo; - var metadata = beatmapInfo.Metadata; + var beatmapInfo = wedge.Beatmap.BeatmapInfo; + var metadata = wedge.beatmap.Metadata; RelativeSizeAxes = Axes.Both; @@ -262,7 +258,7 @@ namespace osu.Game.Screens.Select { Name = "Top-left aligned metadata", Direction = FillDirection.Vertical, - Position = new Vector2(50, 12), + Position = new Vector2(80, 12), Width = .8f, AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, @@ -270,6 +266,7 @@ namespace osu.Game.Screens.Select { TitleLabel = new OsuSpriteText { + Shadow = true, Current = { BindTarget = titleBinding }, Font = OsuFont.TorusAlternate.With(size: 40, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, @@ -277,6 +274,7 @@ namespace osu.Game.Screens.Select }, ArtistLabel = new OsuSpriteText { + Shadow = true, Current = { BindTarget = artistBinding }, //Not sure if this should be semi bold or medium Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), @@ -297,10 +295,10 @@ namespace osu.Game.Screens.Select wedge.starCounter.Colour = s.NewValue >= 6.5 ? colours.Orange1 : Colour4.Black.Opacity(0.75f); wedge.starCounter.Current = (float)s.NewValue; - wedge.difficultyColourBar.FadeColour(colours.ForStarDifficulty(s.NewValue), 750, Easing.OutQuint); + wedge.difficultyColourBar.FadeColour(colours.ForStarDifficulty(s.NewValue)); }, true); - starDifficulty.BindValueChanged(s => + wedge.starDifficulty.BindValueChanged(s => { starRatingDisplay.Current.Value = s.NewValue ?? default; From 65c30d2c2e0a322d0d75003905fd44b4e5969d32 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Sun, 8 Jan 2023 12:56:32 +0100 Subject: [PATCH 0006/2100] Remove nullability disabling --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 62 +++++++++---------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index 1056d9478b..da7cfc6613 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -36,17 +34,11 @@ namespace osu.Game.Screens.Select private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / wedge_height, 0); [Resolved] - private IBindable ruleset { get; set; } + private IBindable ruleset { get; set; } = null!; - [Resolved] - private BeatmapDifficultyCache difficultyCache { get; set; } + protected Container? DisplayedContent { get; private set; } - protected Container DisplayedContent { get; private set; } - - protected WedgeInfoText Info { get; private set; } - - private IBindable starDifficulty = new Bindable(); - private CancellationTokenSource cancellationSource; + protected WedgeInfoText? Info { get; private set; } private readonly Container difficultyColourBar; private readonly StarCounter starCounter; @@ -117,9 +109,9 @@ namespace osu.Game.Screens.Select this.FadeOut(transition_duration * 2, Easing.In); } - private WorkingBeatmap beatmap; + private WorkingBeatmap? beatmap; - public WorkingBeatmap Beatmap + public WorkingBeatmap? Beatmap { get => beatmap; set @@ -127,7 +119,6 @@ namespace osu.Game.Screens.Select if (beatmap == value) return; beatmap = value; - starDifficulty = difficultyCache.GetBindableDifficulty(value.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token); updateDisplay(); } @@ -135,14 +126,7 @@ namespace osu.Game.Screens.Select public override bool IsPresent => base.IsPresent || DisplayedContent == null; // Visibility is updated in the LoadComponentAsync callback - private Container loadingInfo; - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - cancellationSource?.Cancel(); - } + private Container? loadingInfo; private void updateDisplay() { @@ -190,30 +174,36 @@ namespace osu.Game.Screens.Select public partial class WedgeInfoText : Container { - public OsuSpriteText TitleLabel { get; private set; } - public OsuSpriteText ArtistLabel { get; private set; } + public OsuSpriteText TitleLabel { get; private set; } = null!; + public OsuSpriteText ArtistLabel { get; private set; } = null!; - private StarRatingDisplay starRatingDisplay; + private StarRatingDisplay starRatingDisplay = null!; - private ILocalisedBindableString titleBinding; - private ILocalisedBindableString artistBinding; + private ILocalisedBindableString titleBinding = null!; + private ILocalisedBindableString artistBinding = null!; [Resolved] - private IBindable> mods { get; set; } + private IBindable> mods { get; set; } = null!; [Resolved] - private BeatmapInfoWedgeV2 wedge { get; set; } + private BeatmapInfoWedgeV2 wedge { get; set; } = null!; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; - private ModSettingChangeTracker settingChangeTracker; + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } = null!; + + private ModSettingChangeTracker? settingChangeTracker; + + private IBindable? starDifficulty; + private CancellationTokenSource? cancellationSource; [BackgroundDependencyLoader] private void load(LocalisationManager localisation) { - var beatmapInfo = wedge.Beatmap.BeatmapInfo; - var metadata = wedge.beatmap.Metadata; + var beatmapInfo = wedge.Beatmap!.BeatmapInfo; + var metadata = wedge.beatmap!.Metadata; RelativeSizeAxes = Axes.Both; @@ -274,6 +264,7 @@ namespace osu.Game.Screens.Select }, ArtistLabel = new OsuSpriteText { + //figma design has a diffused shadow, instead of the solid one present here. Shadow = true, Current = { BindTarget = artistBinding }, //Not sure if this should be semi bold or medium @@ -298,7 +289,8 @@ namespace osu.Game.Screens.Select wedge.difficultyColourBar.FadeColour(colours.ForStarDifficulty(s.NewValue)); }, true); - wedge.starDifficulty.BindValueChanged(s => + starDifficulty = difficultyCache.GetBindableDifficulty(wedge.beatmap!.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token); + starDifficulty.BindValueChanged(s => { starRatingDisplay.Current.Value = s.NewValue ?? default; @@ -320,6 +312,8 @@ namespace osu.Game.Screens.Select protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); + + cancellationSource?.Cancel(); settingChangeTracker?.Dispose(); } } From 9afdfd7f067c786f236748c84e7028e0c294ff76 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Sun, 8 Jan 2023 15:42:42 +0100 Subject: [PATCH 0007/2100] small tweaks, container edge - effect addition.. --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index da7cfc6613..ced6931e1f 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -27,9 +28,9 @@ namespace osu.Game.Screens.Select public partial class BeatmapInfoWedgeV2 : VisibilityContainer { private const float shear_width = 21; - private const int wedge_height = 120; - + private const float wedge_height = 120; private const float transition_duration = 250; + private const float corner_radius = 10; private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / wedge_height, 0); @@ -45,14 +46,20 @@ namespace osu.Game.Screens.Select public BeatmapInfoWedgeV2() { - CornerRadius = 10; Shear = wedged_container_shear; Masking = true; - Alpha = 0; + EdgeEffect = new EdgeEffectParameters + { + Colour = Colour4.Black.Opacity(.25f), + Type = EdgeEffectType.Shadow, + Radius = corner_radius, + Roundness = corner_radius + }; + CornerRadius = corner_radius; Children = new Drawable[] { - //These elements can't be grouped with the rest of the content, due to being present either outside or under the backgrounds area + // These elements can't be grouped with the rest of the content, due to being present either outside or under the backgrounds area difficultyColourBar = new Container { Colour = Colour4.Transparent, @@ -61,7 +68,7 @@ namespace osu.Game.Screens.Select Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Y, - //By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it. + // By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it. Width = 40, Child = new Box { RelativeSizeAxes = Axes.Both } }, @@ -70,7 +77,7 @@ namespace osu.Game.Screens.Select Colour = Colour4.Transparent, Anchor = Anchor.CentreRight, Origin = Anchor.Centre, - Scale = new Vector2(0.4f), + Scale = new Vector2(0.35f), Shear = -wedged_container_shear, X = -15, Direction = FillDirection.Vertical @@ -85,10 +92,10 @@ namespace osu.Game.Screens.Select float starAngle = (float)(Math.Atan(shear_width / wedge_height) * (180 / Math.PI)); - //Applying the rotation directly to the StarCounter distorts the stars, hence it is applied to the child container + // Applying the rotation directly to the StarCounter distorts the stars, hence it is applied to the child container starCounter.Children.First().Rotation = starAngle; - //Makes sure the stars center themselves properly in the colour bar + // Makes sure the stars center themselves properly in the colour bar starCounter.Children.First().Anchor = Anchor.Centre; starCounter.Children.First().Origin = Anchor.Centre; } @@ -153,13 +160,13 @@ namespace osu.Game.Screens.Select { Masking = true, X = -30, - CornerRadius = 10, + CornerRadius = corner_radius, RelativeSizeAxes = Axes.Both, Depth = DisplayedContent?.Depth + 1 ?? 0, Children = new Drawable[] { new BeatmapInfoWedgeBackground(beatmap) { Shear = -Shear }, - Info = new WedgeInfoText { Shear = -Shear } + Info = new WedgeInfoText(beatmap) { Shear = -Shear } } }, loaded => { @@ -182,11 +189,10 @@ namespace osu.Game.Screens.Select private ILocalisedBindableString titleBinding = null!; private ILocalisedBindableString artistBinding = null!; - [Resolved] - private IBindable> mods { get; set; } = null!; + private readonly WorkingBeatmap working; [Resolved] - private BeatmapInfoWedgeV2 wedge { get; set; } = null!; + private IBindable> mods { get; set; } = null!; [Resolved] private OsuColour colours { get; set; } = null!; @@ -194,16 +200,24 @@ namespace osu.Game.Screens.Select [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } = null!; + [Resolved] + private BeatmapInfoWedgeV2 wedge { get; set; } = null!; + private ModSettingChangeTracker? settingChangeTracker; private IBindable? starDifficulty; private CancellationTokenSource? cancellationSource; + public WedgeInfoText(WorkingBeatmap working) + { + this.working = working; + } + [BackgroundDependencyLoader] private void load(LocalisationManager localisation) { - var beatmapInfo = wedge.Beatmap!.BeatmapInfo; - var metadata = wedge.beatmap!.Metadata; + var beatmapInfo = working.BeatmapInfo; + var metadata = working.Metadata; RelativeSizeAxes = Axes.Both; @@ -249,7 +263,7 @@ namespace osu.Game.Screens.Select Name = "Top-left aligned metadata", Direction = FillDirection.Vertical, Position = new Vector2(80, 12), - Width = .8f, + Width = .7f, AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Children = new Drawable[] @@ -289,7 +303,7 @@ namespace osu.Game.Screens.Select wedge.difficultyColourBar.FadeColour(colours.ForStarDifficulty(s.NewValue)); }, true); - starDifficulty = difficultyCache.GetBindableDifficulty(wedge.beatmap!.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token); + starDifficulty = difficultyCache.GetBindableDifficulty(working.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token); starDifficulty.BindValueChanged(s => { starRatingDisplay.Current.Value = s.NewValue ?? default; From 2a82f618ed9a69dd28613dfc8f04189ceecd702f Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Tue, 10 Jan 2023 17:34:47 +0100 Subject: [PATCH 0008/2100] Add TODO for text margin const, added pertinent comments to known "issues" --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index ced6931e1f..ce07a59a0c 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -32,6 +32,9 @@ namespace osu.Game.Screens.Select private const float transition_duration = 250; private const float corner_radius = 10; + /// Todo: move this const out to song select when more new design elements are implemented for the beatmap details area, since it applies to text alignment of various elements + private const float text_margin = 62; + private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / wedge_height, 0); [Resolved] @@ -159,18 +162,21 @@ namespace osu.Game.Screens.Select LoadComponentAsync(loadingInfo = new Container { Masking = true, + // We offset this by the portion of the colour bar underneath we wish to show X = -30, CornerRadius = corner_radius, RelativeSizeAxes = Axes.Both, Depth = DisplayedContent?.Depth + 1 ?? 0, Children = new Drawable[] { + // TODO: New wedge design uses a coloured horizontal gradient for its background, however this lacks implementation information in the figma draft. + // pending https://www.figma.com/file/DXKwqZhD5yyb1igc3mKo1P?node-id=2980:3361#340801912 being answered. new BeatmapInfoWedgeBackground(beatmap) { Shear = -Shear }, Info = new WedgeInfoText(beatmap) { Shear = -Shear } } }, loaded => { - // ensure we are the most recent loaded wedge. + // Ensure we are the most recent loaded wedge. if (loaded != loadingInfo) return; removeOldInfo(); @@ -262,7 +268,7 @@ namespace osu.Game.Screens.Select { Name = "Top-left aligned metadata", Direction = FillDirection.Vertical, - Position = new Vector2(80, 12), + Position = new Vector2(text_margin + shear_width, 12), Width = .7f, AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, @@ -278,10 +284,10 @@ namespace osu.Game.Screens.Select }, ArtistLabel = new OsuSpriteText { - //figma design has a diffused shadow, instead of the solid one present here. + // TODO : figma design has a diffused shadow, instead of the solid one present here, not possible currently as far as i'm aware. Shadow = true, Current = { BindTarget = artistBinding }, - //Not sure if this should be semi bold or medium + // Not sure if this should be semi bold or medium Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, Truncate = true @@ -297,8 +303,8 @@ namespace osu.Game.Screens.Select starRatingDisplay.DisplayedStars.BindValueChanged(s => { - wedge.starCounter.Colour = s.NewValue >= 6.5 ? colours.Orange1 : Colour4.Black.Opacity(0.75f); wedge.starCounter.Current = (float)s.NewValue; + wedge.starCounter.Colour = s.NewValue >= 6.5 ? colours.Orange1 : Colour4.Black.Opacity(0.75f); wedge.difficultyColourBar.FadeColour(colours.ForStarDifficulty(s.NewValue)); }, true); From 8bfe24ced0c9163c1e1017b76d1f1669d57a160c Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Tue, 10 Jan 2023 17:49:33 +0100 Subject: [PATCH 0009/2100] Remove nullable disable in test. --- .../SongSelect/TestSceneBeatmapInfoWedgeV2.cs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs index 98e9d803ca..193acc8a7b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs @@ -1,10 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; -using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -24,8 +21,8 @@ namespace osu.Game.Tests.Visual.SongSelect [TestFixture] public partial class TestSceneBeatmapInfoWedgeV2 : OsuTestScene { - private RulesetStore rulesets; - private TestBeatmapInfoWedgeV2 infoWedge; + private RulesetStore rulesets = null!; + private TestBeatmapInfoWedgeV2 infoWedge = null!; private readonly List beatmaps = new List(); [BackgroundDependencyLoader] @@ -80,8 +77,8 @@ namespace osu.Game.Tests.Visual.SongSelect private void testBeatmapLabels(Ruleset ruleset) { - AddAssert("check title", () => infoWedge.Info.TitleLabel.Current.Value == $"{ruleset.ShortName}Title"); - AddAssert("check artist", () => infoWedge.Info.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist"); + AddAssert("check title", () => infoWedge.Info!.TitleLabel.Current.Value == $"{ruleset.ShortName}Title"); + AddAssert("check artist", () => infoWedge.Info!.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist"); } [SetUpSteps] @@ -98,7 +95,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void setRuleset(RulesetInfo rulesetInfo) { - Container containerBefore = null; + Container? containerBefore = null; AddStep("set ruleset", () => { @@ -112,9 +109,9 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); } - private void selectBeatmap([CanBeNull] IBeatmap b) + private void selectBeatmap(IBeatmap? b) { - Container containerBefore = null; + Container? containerBefore = null; AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () => { @@ -172,9 +169,9 @@ namespace osu.Game.Tests.Visual.SongSelect private partial class TestBeatmapInfoWedgeV2 : BeatmapInfoWedgeV2 { - public new Container DisplayedContent => base.DisplayedContent; + public new Container? DisplayedContent => base.DisplayedContent; - public new WedgeInfoText Info => base.Info; + public new WedgeInfoText? Info => base.Info; } private class TestHitObject : ConvertHitObject, IHasPosition From 880428046a17606b200750c6f8bdec254fd7e4e1 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Tue, 10 Jan 2023 18:03:28 +0100 Subject: [PATCH 0010/2100] Fix margins on top right aligned elements. --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index ce07a59a0c..1497bed121 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -238,7 +238,7 @@ namespace osu.Game.Screens.Select Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Direction = FillDirection.Vertical, - Padding = new MarginPadding { Top = 14, Right = shear_width / 2 }, + Padding = new MarginPadding { Top = 3, Right = 8 }, AutoSizeAxes = Axes.Both, Shear = wedged_container_shear, Spacing = new Vector2(0f, 5f), From ff5a12fcb4be833fdeb21b406b2ec9e19f88f8ff Mon Sep 17 00:00:00 2001 From: ansel <79257300125@ya.ru> Date: Mon, 16 Jan 2023 20:39:38 +0300 Subject: [PATCH 0011/2100] Localise login form --- osu.Game/Localisation/LoginPanelStrings.cs | 54 ++++++++++++++++++++++ osu.Game/Overlays/Login/LoginForm.cs | 13 +++--- osu.Game/Overlays/Login/LoginPanel.cs | 7 +-- osu.Game/Overlays/Login/UserAction.cs | 10 ++-- 4 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 osu.Game/Localisation/LoginPanelStrings.cs diff --git a/osu.Game/Localisation/LoginPanelStrings.cs b/osu.Game/Localisation/LoginPanelStrings.cs new file mode 100644 index 0000000000..6dfb48fbdc --- /dev/null +++ b/osu.Game/Localisation/LoginPanelStrings.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class LoginPanelStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.LoginPanel"; + + /// + /// "Do not disturb" + /// + public static LocalisableString DoNotDisturb => new TranslatableString(getKey(@"do_not_disturb"), @"Do not disturb"); + + /// + /// "Appear offline" + /// + public static LocalisableString AppearOffline => new TranslatableString(getKey(@"appear_offline"), @"Appear offline"); + + /// + /// "Sign out" + /// + public static LocalisableString SignOut => new TranslatableString(getKey(@"sign_out"), @"Sign out"); + + /// + /// "Signed in" + /// + public static LocalisableString SignedIn => new TranslatableString(getKey(@"signed_in"), @"Signed in"); + + /// + /// "ACCOUNT" + /// + public static LocalisableString Account => new TranslatableString(getKey(@"account"), @"ACCOUNT"); + + /// + /// "Remember username" + /// + public static LocalisableString RememberUsername => new TranslatableString(getKey(@"remember_username"), @"Remember username"); + + /// + /// "Stay signed in" + /// + public static LocalisableString StaySignedIn => new TranslatableString(getKey(@"stay_signed_in"), @"Stay signed in"); + + /// + /// "Register" + /// + public static LocalisableString Register => new TranslatableString(getKey(@"register"), @"Register"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index af145c418c..9f9b8d9342 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -16,6 +16,7 @@ using osu.Game.Online.API; using osu.Game.Overlays.Settings; using osu.Game.Resources.Localisation.Web; using osuTK; +using osu.Game.Localisation; namespace osu.Game.Overlays.Login { @@ -47,7 +48,7 @@ namespace osu.Game.Overlays.Login RelativeSizeAxes = Axes.X; ErrorTextFlowContainer errorText; - LinkFlowContainer forgottenPaswordLink; + LinkFlowContainer forgottenPasswordLink; Children = new Drawable[] { @@ -71,15 +72,15 @@ namespace osu.Game.Overlays.Login }, new SettingsCheckbox { - LabelText = "Remember username", + LabelText = LoginPanelStrings.RememberUsername, Current = config.GetBindable(OsuSetting.SaveUsername), }, new SettingsCheckbox { - LabelText = "Stay signed in", + LabelText = LoginPanelStrings.StaySignedIn, Current = config.GetBindable(OsuSetting.SavePassword), }, - forgottenPaswordLink = new LinkFlowContainer + forgottenPasswordLink = new LinkFlowContainer { Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS }, RelativeSizeAxes = Axes.X, @@ -105,7 +106,7 @@ namespace osu.Game.Overlays.Login }, new SettingsButton { - Text = "Register", + Text = LoginPanelStrings.Register, Action = () => { RequestHide?.Invoke(); @@ -114,7 +115,7 @@ namespace osu.Game.Overlays.Login } }; - forgottenPaswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.WebsiteRootUrl}/home/password-reset"); + forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.WebsiteRootUrl}/home/password-reset"); password.OnCommit += (_, _) => performLogin(); diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 44f2f3273a..fb9987bd82 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -6,6 +6,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; @@ -81,7 +82,7 @@ namespace osu.Game.Overlays.Login { new OsuSpriteText { - Text = "ACCOUNT", + Text = LoginPanelStrings.Account, Margin = new MarginPadding { Bottom = 5 }, Font = OsuFont.GetFont(weight: FontWeight.Bold), }, @@ -115,7 +116,7 @@ namespace osu.Game.Overlays.Login }, }; - linkFlow.AddLink("cancel", api.Logout, string.Empty); + linkFlow.AddLink(Resources.Localisation.Web.CommonStrings.ButtonsCancel.ToLower(), api.Logout, string.Empty); break; case APIState.Online: @@ -140,7 +141,7 @@ namespace osu.Game.Overlays.Login { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = "Signed in", + Text = LoginPanelStrings.SignedIn, Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold), Margin = new MarginPadding { Top = 5, Bottom = 5 }, }, diff --git a/osu.Game/Overlays/Login/UserAction.cs b/osu.Game/Overlays/Login/UserAction.cs index 7a18e38109..813968a053 100644 --- a/osu.Game/Overlays/Login/UserAction.cs +++ b/osu.Game/Overlays/Login/UserAction.cs @@ -1,11 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - -using System.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; +using osu.Game.Localisation; namespace osu.Game.Overlays.Login { @@ -14,13 +12,13 @@ namespace osu.Game.Overlays.Login [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.StatusOnline))] Online, - [Description(@"Do not disturb")] + [LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.DoNotDisturb))] DoNotDisturb, - [Description(@"Appear offline")] + [LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.AppearOffline))] AppearOffline, - [Description(@"Sign out")] + [LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.SignOut))] SignOut, } } From 4c341db33f12a05a1ec6e4abbf60fd4294b0a70d Mon Sep 17 00:00:00 2001 From: ansel <79257300125@ya.ru> Date: Mon, 16 Jan 2023 21:31:01 +0300 Subject: [PATCH 0012/2100] Localise registration window --- .../Localisation/AccountCreationStrings.cs | 77 +++++++++++++++++++ .../Overlays/AccountCreation/ScreenEntry.cs | 19 ++--- .../Overlays/AccountCreation/ScreenWarning.cs | 5 +- .../Overlays/AccountCreation/ScreenWelcome.cs | 9 +-- 4 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 osu.Game/Localisation/AccountCreationStrings.cs diff --git a/osu.Game/Localisation/AccountCreationStrings.cs b/osu.Game/Localisation/AccountCreationStrings.cs new file mode 100644 index 0000000000..4e702ea7cc --- /dev/null +++ b/osu.Game/Localisation/AccountCreationStrings.cs @@ -0,0 +1,77 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class AccountCreationStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.AccountCreation"; + + /// + /// "New Player Registration" + /// + public static LocalisableString NewPlayerRegistration => new TranslatableString(getKey(@"new_player_registration"), @"New Player Registration"); + + /// + /// "let's get you started" + /// + public static LocalisableString LetsGetYouStarted => new TranslatableString(getKey(@"lets_get_you_started"), @"let's get you started"); + + /// + /// "Let's create an account!" + /// + public static LocalisableString LetsCreateAnAccount => new TranslatableString(getKey(@"lets_create_an_account"), @"Let's create an account!"); + + /// + /// "Help, I can't access my account!" + /// + public static LocalisableString HelpICantAccess => new TranslatableString(getKey(@"help_icant_access"), @"Help, I can't access my account!"); + + /// + /// "I understand. This account isn't for me." + /// + public static LocalisableString AccountIsntForMe => new TranslatableString(getKey(@"account_isnt_for_me"), @"I understand. This account isn't for me."); + + /// + /// "email address" + /// + public static LocalisableString EmailAddress => new TranslatableString(getKey(@"email_address"), @"email address"); + + /// + /// "This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!" + /// + public static LocalisableString ThisWillBeYourPublic => new TranslatableString(getKey(@"this_will_be_your_public"), + @"This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!"); + + /// + /// "Will be used for notifications, account verification and in the case you forget your password. No spam, ever." + /// + public static LocalisableString EmailUsage => + new TranslatableString(getKey(@"email_usage"), @"Will be used for notifications, account verification and in the case you forget your password. No spam, ever."); + + /// + /// " Make sure to get it right!" + /// + public static LocalisableString MakeSureToGetIt => new TranslatableString(getKey(@"make_sure_to_get_it"), @" Make sure to get it right!"); + + /// + /// "At least " + /// + public static LocalisableString BeforeCharactersLong => new TranslatableString(getKey(@"before_characters_long"), @"At least "); + + /// + /// "8 characters long" + /// + public static LocalisableString CharactersLong => new TranslatableString(getKey(@"characters_long"), @"8 characters long"); + + /// + /// ". Choose something long but also something you will remember, like a line from your favourite song." + /// + public static LocalisableString AfterCharactersLong => + new TranslatableString(getKey(@"after_characters_long"), @". Choose something long but also something you will remember, like a line from your favourite song."); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index 2e20f83e9e..6718b72805 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -17,6 +17,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Overlays.Settings; using osu.Game.Resources.Localisation.Web; @@ -71,7 +72,7 @@ namespace osu.Game.Overlays.AccountCreation Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Font = OsuFont.GetFont(size: 20), - Text = "Let's create an account!", + Text = AccountCreationStrings.LetsCreateAnAccount }, usernameTextBox = new OsuTextBox { @@ -86,7 +87,7 @@ namespace osu.Game.Overlays.AccountCreation }, emailTextBox = new OsuTextBox { - PlaceholderText = "email address", + PlaceholderText = AccountCreationStrings.EmailAddress, RelativeSizeAxes = Axes.X, TabbableContentContainer = this }, @@ -118,7 +119,7 @@ namespace osu.Game.Overlays.AccountCreation AutoSizeAxes = Axes.Y, Child = new SettingsButton { - Text = "Register", + Text = LoginPanelStrings.Register, Margin = new MarginPadding { Vertical = 20 }, Action = performRegistration } @@ -132,14 +133,14 @@ namespace osu.Game.Overlays.AccountCreation textboxes = new[] { usernameTextBox, emailTextBox, passwordTextBox }; - usernameDescription.AddText("This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!"); + usernameDescription.AddText(AccountCreationStrings.ThisWillBeYourPublic); - emailAddressDescription.AddText("Will be used for notifications, account verification and in the case you forget your password. No spam, ever."); - emailAddressDescription.AddText(" Make sure to get it right!", cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold)); + emailAddressDescription.AddText(AccountCreationStrings.EmailUsage); + emailAddressDescription.AddText(AccountCreationStrings.MakeSureToGetIt, cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold)); - passwordDescription.AddText("At least "); - characterCheckText = passwordDescription.AddText("8 characters long"); - passwordDescription.AddText(". Choose something long but also something you will remember, like a line from your favourite song."); + passwordDescription.AddText(AccountCreationStrings.BeforeCharactersLong); + characterCheckText = passwordDescription.AddText(AccountCreationStrings.CharactersLong); + passwordDescription.AddText(AccountCreationStrings.AfterCharactersLong); passwordTextBox.Current.BindValueChanged(_ => updateCharacterCheckTextColour(), true); characterCheckText.DrawablePartsRecreated += _ => updateCharacterCheckTextColour(); diff --git a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs index a833a871f9..f5807b49b5 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs @@ -17,6 +17,7 @@ using osu.Game.Overlays.Settings; using osu.Game.Screens.Menu; using osuTK; using osuTK.Graphics; +using osu.Game.Localisation; namespace osu.Game.Overlays.AccountCreation { @@ -101,13 +102,13 @@ namespace osu.Game.Overlays.AccountCreation }, new SettingsButton { - Text = "Help, I can't access my account!", + Text = AccountCreationStrings.HelpICantAccess, Margin = new MarginPadding { Top = 50 }, Action = () => game?.OpenUrlExternally(help_centre_url) }, new DangerousSettingsButton { - Text = "I understand. This account isn't for me.", + Text = AccountCreationStrings.AccountIsntForMe, Action = () => this.Push(new ScreenEntry()) }, furtherAssistance = new LinkFlowContainer(cp => cp.Font = cp.Font.With(size: 12)) diff --git a/osu.Game/Overlays/AccountCreation/ScreenWelcome.cs b/osu.Game/Overlays/AccountCreation/ScreenWelcome.cs index 4becb225f8..a81b1019fe 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenWelcome.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenWelcome.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -12,6 +10,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Overlays.Settings; using osu.Game.Screens.Menu; using osuTK; +using osu.Game.Localisation; namespace osu.Game.Overlays.AccountCreation { @@ -46,18 +45,18 @@ namespace osu.Game.Overlays.AccountCreation Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light), - Text = "New Player Registration", + Text = AccountCreationStrings.NewPlayerRegistration, }, new OsuSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Font = OsuFont.GetFont(size: 12), - Text = "let's get you started", + Text = AccountCreationStrings.LetsGetYouStarted, }, new SettingsButton { - Text = "Let's create an account!", + Text = AccountCreationStrings.LetsCreateAnAccount, Margin = new MarginPadding { Vertical = 120 }, Action = () => this.Push(new ScreenWarning()) } From bb3668c76901f5d2109697748b91627fdcb54878 Mon Sep 17 00:00:00 2001 From: ansel <79257300125@ya.ru> Date: Mon, 16 Jan 2023 22:24:03 +0300 Subject: [PATCH 0013/2100] Reuse existing --- osu.Game/Localisation/AccountCreationStrings.cs | 5 ----- osu.Game/Localisation/LoginPanelStrings.cs | 5 ----- osu.Game/Overlays/AccountCreation/ScreenEntry.cs | 2 +- osu.Game/Overlays/Login/UserAction.cs | 2 +- 4 files changed, 2 insertions(+), 12 deletions(-) diff --git a/osu.Game/Localisation/AccountCreationStrings.cs b/osu.Game/Localisation/AccountCreationStrings.cs index 4e702ea7cc..0b27944a61 100644 --- a/osu.Game/Localisation/AccountCreationStrings.cs +++ b/osu.Game/Localisation/AccountCreationStrings.cs @@ -34,11 +34,6 @@ namespace osu.Game.Localisation /// public static LocalisableString AccountIsntForMe => new TranslatableString(getKey(@"account_isnt_for_me"), @"I understand. This account isn't for me."); - /// - /// "email address" - /// - public static LocalisableString EmailAddress => new TranslatableString(getKey(@"email_address"), @"email address"); - /// /// "This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!" /// diff --git a/osu.Game/Localisation/LoginPanelStrings.cs b/osu.Game/Localisation/LoginPanelStrings.cs index 6dfb48fbdc..535d86fbc5 100644 --- a/osu.Game/Localisation/LoginPanelStrings.cs +++ b/osu.Game/Localisation/LoginPanelStrings.cs @@ -19,11 +19,6 @@ namespace osu.Game.Localisation /// public static LocalisableString AppearOffline => new TranslatableString(getKey(@"appear_offline"), @"Appear offline"); - /// - /// "Sign out" - /// - public static LocalisableString SignOut => new TranslatableString(getKey(@"sign_out"), @"Sign out"); - /// /// "Signed in" /// diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index 6718b72805..fc450c7a91 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -87,7 +87,7 @@ namespace osu.Game.Overlays.AccountCreation }, emailTextBox = new OsuTextBox { - PlaceholderText = AccountCreationStrings.EmailAddress, + PlaceholderText = ModelValidationStrings.UserAttributesUserEmail.ToLower(), RelativeSizeAxes = Axes.X, TabbableContentContainer = this }, diff --git a/osu.Game/Overlays/Login/UserAction.cs b/osu.Game/Overlays/Login/UserAction.cs index 813968a053..d4d639f2fb 100644 --- a/osu.Game/Overlays/Login/UserAction.cs +++ b/osu.Game/Overlays/Login/UserAction.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Login [LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.AppearOffline))] AppearOffline, - [LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.SignOut))] + [LocalisableDescription(typeof(UserVerificationStrings), nameof(UserVerificationStrings.BoxInfoLogoutLink))] SignOut, } } From ad32d99daabe400402e59d4046de6de1e95a1d43 Mon Sep 17 00:00:00 2001 From: ansel <79257300125@ya.ru> Date: Mon, 16 Jan 2023 23:08:29 +0300 Subject: [PATCH 0014/2100] Localise caps lock warning --- osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs | 3 ++- osu.Game/Localisation/CommonStrings.cs | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs index 63c98d7838..9de9eceb07 100644 --- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs @@ -16,6 +16,7 @@ using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Platform; +using osu.Game.Localisation; namespace osu.Game.Graphics.UserInterface { @@ -112,7 +113,7 @@ namespace osu.Game.Graphics.UserInterface private partial class CapsWarning : SpriteIcon, IHasTooltip { - public LocalisableString TooltipText => "caps lock is active"; + public LocalisableString TooltipText => CommonStrings.CapsLockIsActive; public CapsWarning() { diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 10178915a2..a68f08efcc 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -154,6 +154,11 @@ namespace osu.Game.Localisation /// public static LocalisableString Exit => new TranslatableString(getKey(@"exit"), @"Exit"); + /// + /// "caps lock is active" + /// + public static LocalisableString CapsLockIsActive => new TranslatableString(getKey(@"caps_lock_is_active"), @"caps lock is active"); + private static string getKey(string key) => $@"{prefix}:{key}"; } -} +} \ No newline at end of file From 7510201804aab33e90d805b8a34b9883f9ff390f Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 16 Jan 2023 22:24:21 +0100 Subject: [PATCH 0015/2100] Add back null beatmap test --- .../Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs index 193acc8a7b..a2935fb218 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -93,6 +94,15 @@ namespace osu.Game.Tests.Visual.SongSelect selectBeatmap(createLongMetadata()); } + [Test] + public void TestNullBeatmap() + { + selectBeatmap(null); + AddAssert("check default title", () => infoWedge.Info!.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title); + AddAssert("check default artist", () => infoWedge.Info!.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist); + AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType().Any()); + } + private void setRuleset(RulesetInfo rulesetInfo) { Container? containerBefore = null; From 74b72e4ac0d6c34df51bf8d27c24d4625a9f8039 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 16 Jan 2023 22:46:18 +0100 Subject: [PATCH 0016/2100] Address issues that joehuu brought up --- .../Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs | 2 +- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs index a2935fb218..4904e2a723 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.SongSelect Add(infoWedge = new TestBeatmapInfoWedgeV2 { - Size = new Vector2(0.6f, 120), + Width = 0.6f, RelativeSizeAxes = Axes.X, Margin = new MarginPadding { Top = 20 } }); diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index 1497bed121..02d640fc0d 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -31,6 +31,7 @@ namespace osu.Game.Screens.Select private const float wedge_height = 120; private const float transition_duration = 250; private const float corner_radius = 10; + private const float colour_bar_width = 30; /// Todo: move this const out to song select when more new design elements are implemented for the beatmap details area, since it applies to text alignment of various elements private const float text_margin = 62; @@ -49,6 +50,7 @@ namespace osu.Game.Screens.Select public BeatmapInfoWedgeV2() { + Height = wedge_height; Shear = wedged_container_shear; Masking = true; EdgeEffect = new EdgeEffectParameters @@ -72,7 +74,7 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.Y, // By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it. - Width = 40, + Width = colour_bar_width + corner_radius, Child = new Box { RelativeSizeAxes = Axes.Both } }, starCounter = new StarCounter @@ -82,7 +84,7 @@ namespace osu.Game.Screens.Select Origin = Anchor.Centre, Scale = new Vector2(0.35f), Shear = -wedged_container_shear, - X = -15, + X = -colour_bar_width / 2, Direction = FillDirection.Vertical } }; @@ -163,7 +165,7 @@ namespace osu.Game.Screens.Select { Masking = true, // We offset this by the portion of the colour bar underneath we wish to show - X = -30, + X = -colour_bar_width, CornerRadius = corner_radius, RelativeSizeAxes = Axes.Both, Depth = DisplayedContent?.Depth + 1 ?? 0, @@ -268,8 +270,7 @@ namespace osu.Game.Screens.Select { Name = "Top-left aligned metadata", Direction = FillDirection.Vertical, - Position = new Vector2(text_margin + shear_width, 12), - Width = .7f, + Padding = new MarginPadding { Horizontal = text_margin + shear_width, Top = 12 }, AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Children = new Drawable[] From df74bccaaa7345e2b016516b799e85dc790a184c Mon Sep 17 00:00:00 2001 From: ansel <79257300125@ya.ru> Date: Tue, 17 Jan 2023 13:31:03 +0300 Subject: [PATCH 0017/2100] Replace 2 strings with one formattable --- osu.Game/Localisation/AccountCreationStrings.cs | 11 +++-------- osu.Game/Overlays/AccountCreation/ScreenEntry.cs | 10 +++++++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game/Localisation/AccountCreationStrings.cs b/osu.Game/Localisation/AccountCreationStrings.cs index 0b27944a61..6acfaaa9ac 100644 --- a/osu.Game/Localisation/AccountCreationStrings.cs +++ b/osu.Game/Localisation/AccountCreationStrings.cs @@ -52,21 +52,16 @@ namespace osu.Game.Localisation public static LocalisableString MakeSureToGetIt => new TranslatableString(getKey(@"make_sure_to_get_it"), @" Make sure to get it right!"); /// - /// "At least " + /// "At least {0}. Choose something long but also something you will remember, like a line from your favourite song." /// - public static LocalisableString BeforeCharactersLong => new TranslatableString(getKey(@"before_characters_long"), @"At least "); + public static LocalisableString PasswordRequirements(string arg0) => new TranslatableString(getKey(@"password_requirements"), + @"At least {0}. Choose something long but also something you will remember, like a line from your favourite song.", arg0); /// /// "8 characters long" /// public static LocalisableString CharactersLong => new TranslatableString(getKey(@"characters_long"), @"8 characters long"); - /// - /// ". Choose something long but also something you will remember, like a line from your favourite song." - /// - public static LocalisableString AfterCharactersLong => - new TranslatableString(getKey(@"after_characters_long"), @". Choose something long but also something you will remember, like a line from your favourite song."); - private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index fc450c7a91..192b95b963 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Framework.Platform; using osu.Framework.Screens; @@ -52,7 +53,7 @@ namespace osu.Game.Overlays.AccountCreation private OsuGame game { get; set; } [BackgroundDependencyLoader] - private void load() + private void load(LocalisationManager localisationManager) { InternalChildren = new Drawable[] { @@ -138,9 +139,12 @@ namespace osu.Game.Overlays.AccountCreation emailAddressDescription.AddText(AccountCreationStrings.EmailUsage); emailAddressDescription.AddText(AccountCreationStrings.MakeSureToGetIt, cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold)); - passwordDescription.AddText(AccountCreationStrings.BeforeCharactersLong); + string[] passwordReq = localisationManager.GetLocalisedBindableString(AccountCreationStrings.PasswordRequirements("{}")).Value.Split("{}"); + if (passwordReq.Length != 2) passwordReq = AccountCreationStrings.PasswordRequirements("{}").ToString().Split("{}"); + + passwordDescription.AddText(passwordReq[0]); characterCheckText = passwordDescription.AddText(AccountCreationStrings.CharactersLong); - passwordDescription.AddText(AccountCreationStrings.AfterCharactersLong); + passwordDescription.AddText(passwordReq[1]); passwordTextBox.Current.BindValueChanged(_ => updateCharacterCheckTextColour(), true); characterCheckText.DrawablePartsRecreated += _ => updateCharacterCheckTextColour(); From 0ac7cd7409e1c61cc0f8df03c1fa0b780adb43da Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Wed, 18 Jan 2023 13:55:52 +0100 Subject: [PATCH 0018/2100] Expose star difficulty to wedge to allow updating starcounter and background colour internally. --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index 02d640fc0d..d90c002953 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -24,7 +24,6 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Screens.Select { - [Cached] public partial class BeatmapInfoWedgeV2 : VisibilityContainer { private const float shear_width = 21; @@ -41,6 +40,9 @@ namespace osu.Game.Screens.Select [Resolved] private IBindable ruleset { get; set; } = null!; + [Resolved] + private OsuColour colours { get; set; } = null!; + protected Container? DisplayedContent { get; private set; } protected WedgeInfoText? Info { get; private set; } @@ -183,6 +185,14 @@ namespace osu.Game.Screens.Select removeOldInfo(); Add(DisplayedContent = loaded); + + Info.StarRatingDisplay.DisplayedStars.BindValueChanged(s => + { + starCounter.Current = (float)s.NewValue; + starCounter.Colour = s.NewValue >= 6.5 ? colours.Orange1 : Colour4.Black.Opacity(0.75f); + + difficultyColourBar.FadeColour(colours.ForStarDifficulty(s.NewValue)); + }, true); }); } } @@ -192,7 +202,7 @@ namespace osu.Game.Screens.Select public OsuSpriteText TitleLabel { get; private set; } = null!; public OsuSpriteText ArtistLabel { get; private set; } = null!; - private StarRatingDisplay starRatingDisplay = null!; + public StarRatingDisplay StarRatingDisplay = null!; private ILocalisedBindableString titleBinding = null!; private ILocalisedBindableString artistBinding = null!; @@ -202,15 +212,9 @@ namespace osu.Game.Screens.Select [Resolved] private IBindable> mods { get; set; } = null!; - [Resolved] - private OsuColour colours { get; set; } = null!; - [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } = null!; - [Resolved] - private BeatmapInfoWedgeV2 wedge { get; set; } = null!; - private ModSettingChangeTracker? settingChangeTracker; private IBindable? starDifficulty; @@ -246,7 +250,7 @@ namespace osu.Game.Screens.Select Spacing = new Vector2(0f, 5f), Children = new Drawable[] { - starRatingDisplay = new StarRatingDisplay(default, animated: true) + StarRatingDisplay = new StarRatingDisplay(default, animated: true) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -302,24 +306,16 @@ namespace osu.Game.Screens.Select { base.LoadComplete(); - starRatingDisplay.DisplayedStars.BindValueChanged(s => - { - wedge.starCounter.Current = (float)s.NewValue; - wedge.starCounter.Colour = s.NewValue >= 6.5 ? colours.Orange1 : Colour4.Black.Opacity(0.75f); - - wedge.difficultyColourBar.FadeColour(colours.ForStarDifficulty(s.NewValue)); - }, true); - starDifficulty = difficultyCache.GetBindableDifficulty(working.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token); starDifficulty.BindValueChanged(s => { - starRatingDisplay.Current.Value = s.NewValue ?? default; + StarRatingDisplay.Current.Value = s.NewValue ?? default; // Don't roll the counter on initial display (but still allow it to roll on applying mods etc.) - if (!starRatingDisplay.IsPresent) - starRatingDisplay.FinishTransforms(true); + if (!StarRatingDisplay.IsPresent) + StarRatingDisplay.FinishTransforms(true); - starRatingDisplay.FadeIn(transition_duration); + StarRatingDisplay.FadeIn(transition_duration); }); mods.BindValueChanged(m => From 655242371b2858371c82970d9fe316eb7d26cd6d Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 23 Jan 2023 17:00:46 +0100 Subject: [PATCH 0019/2100] Buffer wedge content to avoid opacity issues when showing / hiding --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index d90c002953..63e414d6ad 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -49,6 +49,7 @@ namespace osu.Game.Screens.Select private readonly Container difficultyColourBar; private readonly StarCounter starCounter; + private readonly BufferedContainer bufferedContent; public BeatmapInfoWedgeV2() { @@ -64,30 +65,35 @@ namespace osu.Game.Screens.Select }; CornerRadius = corner_radius; - Children = new Drawable[] + // We want to buffer the wedge to avoid weird transparency overlaps between the colour bar and the background. + Child = bufferedContent = new BufferedContainer { - // These elements can't be grouped with the rest of the content, due to being present either outside or under the backgrounds area - difficultyColourBar = new Container + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Colour = Colour4.Transparent, - Depth = float.MaxValue, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Y, + // These elements can't be grouped with the rest of the content, due to being present either outside or under the backgrounds area + difficultyColourBar = new Container + { + Colour = Colour4.Transparent, + Depth = float.MaxValue, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Y, - // By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it. - Width = colour_bar_width + corner_radius, - Child = new Box { RelativeSizeAxes = Axes.Both } - }, - starCounter = new StarCounter - { - Colour = Colour4.Transparent, - Anchor = Anchor.CentreRight, - Origin = Anchor.Centre, - Scale = new Vector2(0.35f), - Shear = -wedged_container_shear, - X = -colour_bar_width / 2, - Direction = FillDirection.Vertical + // By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it. + Width = colour_bar_width + corner_radius, + Child = new Box { RelativeSizeAxes = Axes.Both } + }, + starCounter = new StarCounter + { + Colour = Colour4.Transparent, + Anchor = Anchor.CentreRight, + Origin = Anchor.Centre, + Scale = new Vector2(0.35f), + Shear = -wedged_container_shear, + X = -colour_bar_width / 2, + Direction = FillDirection.Vertical + } } }; } @@ -184,9 +190,9 @@ namespace osu.Game.Screens.Select if (loaded != loadingInfo) return; removeOldInfo(); - Add(DisplayedContent = loaded); + bufferedContent.Add(DisplayedContent = loaded); - Info.StarRatingDisplay.DisplayedStars.BindValueChanged(s => + Info.DisplayedStars.BindValueChanged(s => { starCounter.Current = (float)s.NewValue; starCounter.Colour = s.NewValue >= 6.5 ? colours.Orange1 : Colour4.Black.Opacity(0.75f); @@ -202,13 +208,15 @@ namespace osu.Game.Screens.Select public OsuSpriteText TitleLabel { get; private set; } = null!; public OsuSpriteText ArtistLabel { get; private set; } = null!; - public StarRatingDisplay StarRatingDisplay = null!; + private StarRatingDisplay starRatingDisplay = null!; private ILocalisedBindableString titleBinding = null!; private ILocalisedBindableString artistBinding = null!; private readonly WorkingBeatmap working; + public IBindable DisplayedStars => starRatingDisplay.DisplayedStars; + [Resolved] private IBindable> mods { get; set; } = null!; @@ -250,7 +258,7 @@ namespace osu.Game.Screens.Select Spacing = new Vector2(0f, 5f), Children = new Drawable[] { - StarRatingDisplay = new StarRatingDisplay(default, animated: true) + starRatingDisplay = new StarRatingDisplay(default, animated: true) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -309,13 +317,13 @@ namespace osu.Game.Screens.Select starDifficulty = difficultyCache.GetBindableDifficulty(working.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token); starDifficulty.BindValueChanged(s => { - StarRatingDisplay.Current.Value = s.NewValue ?? default; + starRatingDisplay.Current.Value = s.NewValue ?? default; // Don't roll the counter on initial display (but still allow it to roll on applying mods etc.) - if (!StarRatingDisplay.IsPresent) - StarRatingDisplay.FinishTransforms(true); + if (!starRatingDisplay.IsPresent) + starRatingDisplay.FinishTransforms(true); - StarRatingDisplay.FadeIn(transition_duration); + starRatingDisplay.FadeIn(transition_duration); }); mods.BindValueChanged(m => From c7d49bdc82c4ab48ab60040cdd0e161a481fdf94 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 30 Jan 2023 13:13:57 +0100 Subject: [PATCH 0020/2100] Update ```BeatmapInfoWedgeV2.cs``` animation to be similar to exit transition in ```SongSelect.cs`` --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index 63e414d6ad..6a1662fd6a 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -113,20 +113,18 @@ namespace osu.Game.Screens.Select starCounter.Children.First().Origin = Anchor.Centre; } - private const double animation_duration = 800; + private const double animation_duration = 600; protected override void PopIn() { this.MoveToX(0, animation_duration, Easing.OutQuint); - this.RotateTo(0, animation_duration, Easing.OutQuint); - this.FadeIn(transition_duration); + this.FadeIn(200, Easing.In); } protected override void PopOut() { - this.MoveToX(-100, animation_duration, Easing.In); - this.RotateTo(10, animation_duration, Easing.In); - this.FadeOut(transition_duration * 2, Easing.In); + this.MoveToX(-150, animation_duration, Easing.OutQuint); + this.FadeOut(200, Easing.OutQuint); } private WorkingBeatmap? beatmap; From 92690afa5fecd750583924c5917fc3b877b73041 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 30 Jan 2023 16:12:26 +0100 Subject: [PATCH 0021/2100] de-nest ```removeOldInfo()``` --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index 6a1662fd6a..41fe9d8d50 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -152,15 +152,6 @@ namespace osu.Game.Screens.Select void perform() { - void removeOldInfo() - { - State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible; - - DisplayedContent?.FadeOut(transition_duration); - DisplayedContent?.Expire(); - DisplayedContent = null; - } - if (beatmap == null) { removeOldInfo(); @@ -199,6 +190,15 @@ namespace osu.Game.Screens.Select }, true); }); } + + void removeOldInfo() + { + State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible; + + DisplayedContent?.FadeOut(transition_duration); + DisplayedContent?.Expire(); + DisplayedContent = null; + } } public partial class WedgeInfoText : Container From 27c52a45fc079174f29067ae6b7e97529f02cceb Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 30 Jan 2023 16:13:55 +0100 Subject: [PATCH 0022/2100] Use inline lambda for scheduling --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index 41fe9d8d50..0cc60e4bba 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -148,9 +148,7 @@ namespace osu.Game.Screens.Select private void updateDisplay() { - Scheduler.AddOnce(perform); - - void perform() + Scheduler.AddOnce(() => { if (beatmap == null) { @@ -189,7 +187,7 @@ namespace osu.Game.Screens.Select difficultyColourBar.FadeColour(colours.ForStarDifficulty(s.NewValue)); }, true); }); - } + }); void removeOldInfo() { From 5fc8f1d1bef944c43f1e9b1ac0456ded7015db72 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Fri, 3 Feb 2023 19:52:01 +0100 Subject: [PATCH 0023/2100] Fix ```BeatmapInfoWedgeV2.cs``` starCounter needing janky rotation application --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index 0cc60e4bba..48a16d5449 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using osuTK; using osu.Framework.Allocation; @@ -84,15 +83,25 @@ namespace osu.Game.Screens.Select Width = colour_bar_width + corner_radius, Child = new Box { RelativeSizeAxes = Axes.Both } }, - starCounter = new StarCounter + new Container { - Colour = Colour4.Transparent, - Anchor = Anchor.CentreRight, - Origin = Anchor.Centre, - Scale = new Vector2(0.35f), + // Applying the shear to this container and nesting the starCounter inside avoids + // the deformation that occurs if the shear is applied to the starCounter whilst rotated Shear = -wedged_container_shear, X = -colour_bar_width / 2, - Direction = FillDirection.Vertical + Anchor = Anchor.CentreRight, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Width = colour_bar_width, + Child = starCounter = new StarCounter + { + Rotation = (float)(Math.Atan(shear_width / wedge_height) * (180 / Math.PI)), + Colour = Colour4.Transparent, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.35f), + Direction = FillDirection.Vertical + } } } }; @@ -102,15 +111,6 @@ namespace osu.Game.Screens.Select private void load() { ruleset.BindValueChanged(_ => updateDisplay()); - - float starAngle = (float)(Math.Atan(shear_width / wedge_height) * (180 / Math.PI)); - - // Applying the rotation directly to the StarCounter distorts the stars, hence it is applied to the child container - starCounter.Children.First().Rotation = starAngle; - - // Makes sure the stars center themselves properly in the colour bar - starCounter.Children.First().Anchor = Anchor.Centre; - starCounter.Children.First().Origin = Anchor.Centre; } private const double animation_duration = 600; From de37a0a000bfb99ad0f56eaadbe9929eb93229ab Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Fri, 3 Feb 2023 19:53:04 +0100 Subject: [PATCH 0024/2100] enable pixelSnapping for the ```BufferedContainer``` in BeatmapInfoWedgeV2.cs --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index 48a16d5449..a18d4086f7 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -65,7 +65,7 @@ namespace osu.Game.Screens.Select CornerRadius = corner_radius; // We want to buffer the wedge to avoid weird transparency overlaps between the colour bar and the background. - Child = bufferedContent = new BufferedContainer + Child = bufferedContent = new BufferedContainer(pixelSnapping: true) { RelativeSizeAxes = Axes.Both, Children = new Drawable[] From 38cc47d64ec35313d7cb928ae78a018b5641e2d5 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Sat, 4 Feb 2023 16:52:30 +0100 Subject: [PATCH 0025/2100] Remove ```IsPresent``` usages --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index a18d4086f7..fda20dde4d 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -142,8 +142,6 @@ namespace osu.Game.Screens.Select } } - public override bool IsPresent => base.IsPresent || DisplayedContent == null; // Visibility is updated in the LoadComponentAsync callback - private Container? loadingInfo; private void updateDisplay() @@ -316,7 +314,7 @@ namespace osu.Game.Screens.Select starRatingDisplay.Current.Value = s.NewValue ?? default; // Don't roll the counter on initial display (but still allow it to roll on applying mods etc.) - if (!starRatingDisplay.IsPresent) + if (starRatingDisplay.Alpha > 0) starRatingDisplay.FinishTransforms(true); starRatingDisplay.FadeIn(transition_duration); From cb679ccc2b95858b0ff85e86bffcd6ee6a27d9dd Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Sat, 11 Feb 2023 18:00:17 +0100 Subject: [PATCH 0026/2100] Separate wedge visibility test into its own method --- .../SongSelect/TestSceneBeatmapInfoWedgeV2.cs | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs index 4904e2a723..f99950dfb0 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -49,12 +50,6 @@ namespace osu.Game.Tests.Visual.SongSelect AddWaitStep("wait for select", 3); - AddStep("hide", () => { infoWedge.Hide(); }); - - AddWaitStep("wait for hide", 3); - - AddStep("show", () => { infoWedge.Show(); }); - AddSliderStep("change star difficulty", 0, 11.9, 5.55, v => { foreach (var hasCurrentValue in infoWedge.Info.ChildrenOfType>()) @@ -76,6 +71,26 @@ namespace osu.Game.Tests.Visual.SongSelect } } + [Test] + public void TestWedgeVisibility() + { + AddStep("Make shadow red for test visibility", () => + { + infoWedge.EdgeEffect = new EdgeEffectParameters + { + Colour = Colour4.Red, + Type = EdgeEffectType.Shadow, + Radius = 5, + }; + }); + AddStep("hide", () => { infoWedge.Hide(); }); + AddWaitStep("wait for hide", 3); + AddAssert("check visibility", () => infoWedge.Alpha == 0); + AddStep("show", () => { infoWedge.Show(); }); + AddWaitStep("wait for show", 1); + AddAssert("check visibility", () => infoWedge.Alpha > 0); + } + private void testBeatmapLabels(Ruleset ruleset) { AddAssert("check title", () => infoWedge.Info!.TitleLabel.Current.Value == $"{ruleset.ShortName}Title"); From 468419896a5364252afa23263d9e43d26c4b7edf Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Sat, 11 Feb 2023 18:08:50 +0100 Subject: [PATCH 0027/2100] Separate ruleset changing tests into their own method. Add small clarification for edge colouring in visibility test --- .../SongSelect/TestSceneBeatmapInfoWedgeV2.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs index f99950dfb0..ebd8c008b3 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs @@ -39,22 +39,25 @@ namespace osu.Game.Tests.Visual.SongSelect Add(infoWedge = new TestBeatmapInfoWedgeV2 { + State = { Value = Visibility.Visible }, Width = 0.6f, RelativeSizeAxes = Axes.X, Margin = new MarginPadding { Top = 20 } }); - AddStep("show", () => infoWedge.Show()); - - selectBeatmap(Beatmap.Value.Beatmap); - - AddWaitStep("wait for select", 3); - AddSliderStep("change star difficulty", 0, 11.9, 5.55, v => { foreach (var hasCurrentValue in infoWedge.Info.ChildrenOfType>()) hasCurrentValue.Current.Value = new StarDifficulty(v, 0); }); + } + + [Test] + public void TestRulesetChange() + { + selectBeatmap(Beatmap.Value.Beatmap); + + AddWaitStep("wait for select", 3); foreach (var rulesetInfo in rulesets.AvailableRulesets) { @@ -74,6 +77,8 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestWedgeVisibility() { + // Mostly just in case someone runs this test before others, + // leading to the shadow being very hard to see if it is black AddStep("Make shadow red for test visibility", () => { infoWedge.EdgeEffect = new EdgeEffectParameters From 09cb6ca3a797e516c3097b48883468be0f85f237 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Sat, 11 Feb 2023 18:15:21 +0100 Subject: [PATCH 0028/2100] Clean up formatting and wedge placement in testscene a tad, --- .../Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs index ebd8c008b3..3f3c7441f4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelect State = { Value = Visibility.Visible }, Width = 0.6f, RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = 20 } + Margin = new MarginPadding { Top = 20, Left = -10 } }); AddSliderStep("change star difficulty", 0, 11.9, 5.55, v => @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.SongSelect { // Mostly just in case someone runs this test before others, // leading to the shadow being very hard to see if it is black - AddStep("Make shadow red for test visibility", () => + AddStep("make shadow red for test visibility", () => { infoWedge.EdgeEffect = new EdgeEffectParameters { From 299023fce036d6995ead30d00d1de6364bbcc137 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Tue, 21 Feb 2023 16:07:26 +0100 Subject: [PATCH 0029/2100] Improve visibility of wedge shading in test scene and fix an issue with excessive roundness on said shadow. --- .../SongSelect/TestSceneBeatmapInfoWedgeV2.cs | 25 ++++++++++++++----- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 9 +++---- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs index 3f3c7441f4..09b93119cc 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -37,12 +38,25 @@ namespace osu.Game.Tests.Visual.SongSelect { base.LoadComplete(); - Add(infoWedge = new TestBeatmapInfoWedgeV2 + AddRange(new Drawable[] { - State = { Value = Visibility.Visible }, - Width = 0.6f, - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = 20, Left = -10 } + // This exists only to make the wedge more visible in the test scene + new Box + { + Y = -20, + Colour = Colour4.Cornsilk.Darken(0.2f), + Height = BeatmapInfoWedgeV2.WEDGE_HEIGHT + 40, + Width = 0.65f, + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Top = 20, Left = -10 } + }, + infoWedge = new TestBeatmapInfoWedgeV2 + { + State = { Value = Visibility.Visible }, + Width = 0.6f, + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Top = 20, Left = -10 } + }, }); AddSliderStep("change star difficulty", 0, 11.9, 5.55, v => @@ -200,7 +214,6 @@ namespace osu.Game.Tests.Visual.SongSelect private partial class TestBeatmapInfoWedgeV2 : BeatmapInfoWedgeV2 { public new Container? DisplayedContent => base.DisplayedContent; - public new WedgeInfoText? Info => base.Info; } diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index fda20dde4d..0a35e68c7e 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -25,8 +25,8 @@ namespace osu.Game.Screens.Select { public partial class BeatmapInfoWedgeV2 : VisibilityContainer { + public const float WEDGE_HEIGHT = 120; private const float shear_width = 21; - private const float wedge_height = 120; private const float transition_duration = 250; private const float corner_radius = 10; private const float colour_bar_width = 30; @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Select /// Todo: move this const out to song select when more new design elements are implemented for the beatmap details area, since it applies to text alignment of various elements private const float text_margin = 62; - private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / wedge_height, 0); + private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / WEDGE_HEIGHT, 0); [Resolved] private IBindable ruleset { get; set; } = null!; @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Select public BeatmapInfoWedgeV2() { - Height = wedge_height; + Height = WEDGE_HEIGHT; Shear = wedged_container_shear; Masking = true; EdgeEffect = new EdgeEffectParameters @@ -60,7 +60,6 @@ namespace osu.Game.Screens.Select Colour = Colour4.Black.Opacity(.25f), Type = EdgeEffectType.Shadow, Radius = corner_radius, - Roundness = corner_radius }; CornerRadius = corner_radius; @@ -95,7 +94,7 @@ namespace osu.Game.Screens.Select Width = colour_bar_width, Child = starCounter = new StarCounter { - Rotation = (float)(Math.Atan(shear_width / wedge_height) * (180 / Math.PI)), + Rotation = (float)(Math.Atan(shear_width / WEDGE_HEIGHT) * (180 / Math.PI)), Colour = Colour4.Transparent, Anchor = Anchor.Centre, Origin = Anchor.Centre, From f21238f517fa8e22a694affd4406bf3b858a8bb4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Mar 2023 16:51:57 +0900 Subject: [PATCH 0030/2100] Adjust shadow to look better --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index 0a35e68c7e..b7b60cffab 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -57,9 +57,9 @@ namespace osu.Game.Screens.Select Masking = true; EdgeEffect = new EdgeEffectParameters { - Colour = Colour4.Black.Opacity(.25f), + Colour = Colour4.Black.Opacity(0.2f), Type = EdgeEffectType.Shadow, - Radius = corner_radius, + Radius = 3, }; CornerRadius = corner_radius; From 0991c56e1c5cd9f9713ea7c55e9b9ce8fde3c37b Mon Sep 17 00:00:00 2001 From: Renzo Poggio Date: Tue, 25 Apr 2023 00:05:15 -0300 Subject: [PATCH 0031/2100] Add extra check in 'SelectPreviousRandom' Check if the poped beatmap exists within the beatmapSets --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 6ba9843f7b..8c0f67564a 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -540,7 +540,7 @@ namespace osu.Game.Screens.Select { var beatmap = randomSelectedBeatmaps.Pop(); - if (!beatmap.Filtered.Value) + if (!beatmap.Filtered.Value && beatmapSets.Any(beatset => beatset.Beatmaps.Contains(beatmap))) { if (selectedBeatmapSet != null) { From 3c6141f233d0eb829c831fea7d838f7f2da2ecbd Mon Sep 17 00:00:00 2001 From: Cootz Date: Thu, 27 Apr 2023 17:08:29 +0300 Subject: [PATCH 0032/2100] Add `ModSearch` --- osu.Game/Overlays/Mods/ModSearch.cs | 8 ++++++++ osu.Game/Overlays/Mods/ModSelectOverlay.cs | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 osu.Game/Overlays/Mods/ModSearch.cs diff --git a/osu.Game/Overlays/Mods/ModSearch.cs b/osu.Game/Overlays/Mods/ModSearch.cs new file mode 100644 index 0000000000..c96be4d817 --- /dev/null +++ b/osu.Game/Overlays/Mods/ModSearch.cs @@ -0,0 +1,8 @@ +// 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.Overlays.Mods; + +public partial class ModSearch +{ +} diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 16602db4be..edf16c09fb 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -188,8 +188,8 @@ namespace osu.Game.Overlays.Mods { MainAreaContent.Add(new Container { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, AutoSizeAxes = Axes.X, Height = ModsEffectDisplay.HEIGHT, Margin = new MarginPadding { Horizontal = 100 }, @@ -197,7 +197,7 @@ namespace osu.Game.Overlays.Mods { Anchor = Anchor.Centre, Origin = Anchor.Centre - }, + } }); } From b795e8ac5a25eac8619bee8fbfe510d2edd91573 Mon Sep 17 00:00:00 2001 From: Cootz Date: Thu, 27 Apr 2023 17:26:35 +0300 Subject: [PATCH 0033/2100] Use `ModSearch` in `ModeSelectOverlay` --- osu.Game/Overlays/Mods/ModSearch.cs | 4 +++- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSearch.cs b/osu.Game/Overlays/Mods/ModSearch.cs index c96be4d817..0a82d967af 100644 --- a/osu.Game/Overlays/Mods/ModSearch.cs +++ b/osu.Game/Overlays/Mods/ModSearch.cs @@ -1,8 +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 osu.Framework.Graphics.Containers; + namespace osu.Game.Overlays.Mods; -public partial class ModSearch +public partial class ModSearch : Container { } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index edf16c09fb..31f2c87100 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -110,6 +110,8 @@ namespace osu.Game.Overlays.Mods private DifficultyMultiplierDisplay? multiplierDisplay; + private ModSearch? modSearch; + protected ShearedButton BackButton { get; private set; } = null!; protected ShearedToggleButton? CustomisationButton { get; private set; } @@ -201,6 +203,16 @@ namespace osu.Game.Overlays.Mods }); } + MainAreaContent.Add(new Container + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.X, + Height = ModsEffectDisplay.HEIGHT, + Margin = new MarginPadding { Horizontal = 100 }, + Child = modSearch = new ModSearch() + }); + FooterContent.Child = footerButtonFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, From ae85e2a3ff94a6c43282bf7ea084baff1cc060f9 Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 28 Apr 2023 19:51:10 +0800 Subject: [PATCH 0034/2100] Update LoginForm.cs --- osu.Game/Overlays/Login/LoginForm.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index af145c418c..5f27db90e9 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -42,10 +42,14 @@ namespace osu.Game.Overlays.Login private void load(OsuConfigManager config, AccountCreationOverlay accountCreation) { Direction = FillDirection.Vertical; - Spacing = new Vector2(0, 5); + Spacing = new Vector2(0, SettingsSection.ITEM_SPACING); AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; - + Padding = new MarginPadding + { + Top = 5, + Bottom = 24, + }; ErrorTextFlowContainer errorText; LinkFlowContainer forgottenPaswordLink; @@ -81,7 +85,11 @@ namespace osu.Game.Overlays.Login }, forgottenPaswordLink = new LinkFlowContainer { - Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS }, + Padding = new MarginPadding + { + Left = SettingsPanel.CONTENT_MARGINS, + Bottom = SettingsSection.ITEM_SPACING + }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, }, From a6ca0497399d5e83faca452782c49fa74c2c406a Mon Sep 17 00:00:00 2001 From: Cootz Date: Tue, 2 May 2023 14:15:33 +0300 Subject: [PATCH 0035/2100] Manually implement @bdach prototype --- .../UserInterface/TestSceneModColumn.cs | 2 +- .../Mods/Input/ClassicModHotkeyHandler.cs | 2 +- .../Mods/Input/SequentialModHotkeyHandler.cs | 2 +- osu.Game/Overlays/Mods/ModColumn.cs | 12 +++--- osu.Game/Overlays/Mods/ModPanel.cs | 30 +++++++++++--- osu.Game/Overlays/Mods/ModPresetPanel.cs | 11 ++++++ osu.Game/Overlays/Mods/ModSelectColumn.cs | 9 ++++- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 39 ++++++++++++------- osu.Game/Overlays/Mods/ModSelectPanel.cs | 26 ++++++++++++- osu.Game/Overlays/Mods/ModState.cs | 11 +++++- osu.Game/Overlays/Mods/SelectAllModsButton.cs | 2 +- 11 files changed, 112 insertions(+), 34 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index a11000214c..3f5676ee24 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -291,7 +291,7 @@ namespace osu.Game.Tests.Visual.UserInterface private void setFilter(Func? filter) { foreach (var modState in this.ChildrenOfType().Single().AvailableMods) - modState.Filtered.Value = filter?.Invoke(modState.Mod) == false; + modState.MatchingFilter.Value = filter?.Invoke(modState.Mod) == false; } private partial class TestModColumn : ModColumn diff --git a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs index 4f3c18fc43..566e741880 100644 --- a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs +++ b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs @@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Mods.Input if (!mod_type_lookup.TryGetValue(e.Key, out var typesToMatch)) return false; - var matchingMods = availableMods.Where(modState => matches(modState, typesToMatch) && !modState.Filtered.Value).ToArray(); + var matchingMods = availableMods.Where(modState => matches(modState, typesToMatch) && modState.MatchingFilter.Value).ToArray(); if (matchingMods.Length == 0) return false; diff --git a/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs index dedb556304..b4ec8ad4c8 100644 --- a/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs +++ b/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Mods.Input if (index < 0) return false; - var modState = availableMods.Where(modState => !modState.Filtered.Value).ElementAtOrDefault(index); + var modState = availableMods.Where(modState => modState.MatchingFilter.Value).ElementAtOrDefault(index); if (modState == null) return false; diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 5d9f616e5f..df2a7780d3 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Mods foreach (var mod in availableMods) { mod.Active.BindValueChanged(_ => updateState()); - mod.Filtered.BindValueChanged(_ => updateState()); + mod.MatchingFilter.BindValueChanged(_ => updateState()); } updateState(); @@ -145,12 +145,12 @@ namespace osu.Game.Overlays.Mods private void updateState() { - Alpha = availableMods.All(mod => mod.Filtered.Value) ? 0 : 1; + Alpha = availableMods.All(mod => !mod.MatchingFilter.Value) ? 0 : 1; if (toggleAllCheckbox != null && !SelectionAnimationRunning) { - toggleAllCheckbox.Alpha = availableMods.Any(panel => !panel.Filtered.Value) ? 1 : 0; - toggleAllCheckbox.Current.Value = availableMods.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value); + toggleAllCheckbox.Alpha = availableMods.Any(panel => panel.MatchingFilter.Value) ? 1 : 0; + toggleAllCheckbox.Current.Value = availableMods.Where(panel => panel.MatchingFilter.Value).All(panel => panel.Active.Value); } } @@ -195,7 +195,7 @@ namespace osu.Game.Overlays.Mods { pendingSelectionOperations.Clear(); - foreach (var button in availableMods.Where(b => !b.Active.Value && !b.Filtered.Value)) + foreach (var button in availableMods.Where(b => !b.Active.Value && b.MatchingFilter.Value)) pendingSelectionOperations.Enqueue(() => button.Active.Value = true); } @@ -206,7 +206,7 @@ namespace osu.Game.Overlays.Mods { pendingSelectionOperations.Clear(); - foreach (var button in availableMods.Where(b => b.Active.Value && !b.Filtered.Value)) + foreach (var button in availableMods.Where(b => b.Active.Value && b.MatchingFilter.Value)) pendingSelectionOperations.Enqueue(() => button.Active.Value = false); } diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index b5fee9d116..a5ff52dfd2 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -1,9 +1,12 @@ // 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -11,11 +14,10 @@ using osuTK; namespace osu.Game.Overlays.Mods { - public partial class ModPanel : ModSelectPanel + public partial class ModPanel : ModSelectPanel, IConditionalFilterable { public Mod Mod => modState.Mod; public override BindableBool Active => modState.Active; - public BindableBool Filtered => modState.Filtered; protected override float IdleSwitchWidth => 54; protected override float ExpandedSwitchWidth => 70; @@ -54,7 +56,7 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); - Filtered.BindValueChanged(_ => updateFilterState(), true); + canBeShown.BindTo(modState.ValidForSelection); } protected override void Select() @@ -71,11 +73,29 @@ namespace osu.Game.Overlays.Mods #region Filtering support - private void updateFilterState() + public override IEnumerable FilterTerms => new[] { - this.FadeTo(Filtered.Value ? 0 : 1); + Mod.Name, + Mod.Acronym, + Mod.Description + }; + + public override bool MatchingFilter + { + get => modState.MatchingFilter.Value; + set + { + if (modState.MatchingFilter.Value == value) + return; + + modState.MatchingFilter.Value = value; + this.FadeTo(value ? 1 : 0); + } } + private readonly BindableBool canBeShown = new BindableBool(true); + IBindable IConditionalFilterable.CanBeShown => canBeShown; + #endregion } } diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index 6e12e34124..5b6146e106 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics; @@ -80,6 +81,16 @@ namespace osu.Game.Overlays.Mods Active.Value = new HashSet(Preset.Value.Mods).SetEquals(selectedMods.Value); } + #region Filtering support + + public override IEnumerable FilterTerms => new LocalisableString[] + { + Preset.Value.Name, + Preset.Value.Description + }; + + #endregion + #region IHasCustomTooltip public ModPreset TooltipContent => Preset.Value; diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index e6d7bcd97d..8bf3fd404f 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -43,10 +43,15 @@ namespace osu.Game.Overlays.Mods /// public readonly Bindable Active = new BindableBool(true); + public string SearchTerm + { + set => ItemsFlow.SearchTerm = value; + } + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value; protected readonly Container ControlContainer; - protected readonly FillFlowContainer ItemsFlow; + protected readonly SearchContainer ItemsFlow; private readonly TextFlowContainer headerText; private readonly Box headerBackground; @@ -150,7 +155,7 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.Both, ClampExtension = 100, ScrollbarOverlapsContent = false, - Child = ItemsFlow = new FillFlowContainer + Child = ItemsFlow = new SearchContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 31f2c87100..12e894cfba 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -108,6 +108,8 @@ namespace osu.Game.Overlays.Mods private ColumnFlowContainer columnFlow = null!; private FillFlowContainer footerButtonFlow = null!; + private Container aboveColumnsContent = null!; + private ShearedSearchTextBox searchTextBox = null!; private DifficultyMultiplierDisplay? multiplierDisplay; private ModSearch? modSearch; @@ -148,6 +150,17 @@ namespace osu.Game.Overlays.Mods MainAreaContent.AddRange(new Drawable[] { + aboveColumnsContent = new Container + { + RelativeSizeAxes = Axes.X, + Height = ModsEffectDisplay.HEIGHT, + Padding = new MarginPadding { Horizontal = 100 }, + Child = searchTextBox = new ShearedSearchTextBox + { + HoldFocus = false, + Width = 300 + } + }, new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, @@ -188,18 +201,10 @@ namespace osu.Game.Overlays.Mods if (ShowTotalMultiplier) { - MainAreaContent.Add(new Container + aboveColumnsContent.Add(multiplierDisplay = new DifficultyMultiplierDisplay { - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - AutoSizeAxes = Axes.X, - Height = ModsEffectDisplay.HEIGHT, - Margin = new MarginPadding { Horizontal = 100 }, - Child = multiplierDisplay = new DifficultyMultiplierDisplay - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre }); } @@ -271,6 +276,12 @@ namespace osu.Game.Overlays.Mods customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); + searchTextBox.Current.BindValueChanged(query => + { + foreach (var column in columnFlow.Columns) + column.SearchTerm = query.NewValue; + }, true); + // Start scrolled slightly to the right to give the user a sense that // there is more horizontal content available. ScheduleAfterChildren(() => @@ -352,7 +363,7 @@ namespace osu.Game.Overlays.Mods private void filterMods() { foreach (var modState in allAvailableMods) - modState.Filtered.Value = !modState.Mod.HasImplementation || !IsValidMod.Invoke(modState.Mod); + modState.ValidForSelection.Value = modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod); } private void updateMultiplier() @@ -487,7 +498,7 @@ namespace osu.Game.Overlays.Mods { var column = columnFlow[i].Column; - bool allFiltered = column is ModColumn modColumn && modColumn.AvailableMods.All(modState => modState.Filtered.Value); + bool allFiltered = column is ModColumn modColumn && modColumn.AvailableMods.All(modState => !modState.MatchingFilter.Value); double delay = allFiltered ? 0 : nonFilteredColumnCount * 30; double duration = allFiltered ? 0 : fade_in_duration; @@ -549,7 +560,7 @@ namespace osu.Game.Overlays.Mods if (column is ModColumn modColumn) { - allFiltered = modColumn.AvailableMods.All(modState => modState.Filtered.Value); + allFiltered = modColumn.AvailableMods.All(modState => !modState.MatchingFilter.Value); modColumn.FlushPendingSelections(); } diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index 81285833bd..90663d083c 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.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 osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -24,7 +25,7 @@ using osuTK.Input; namespace osu.Game.Overlays.Mods { - public abstract partial class ModSelectPanel : OsuClickableContainer, IHasAccentColour + public abstract partial class ModSelectPanel : OsuClickableContainer, IHasAccentColour, IFilterable { public abstract BindableBool Active { get; } @@ -263,5 +264,28 @@ namespace osu.Game.Overlays.Mods TextBackground.FadeColour(foregroundColour, transitionDuration, Easing.OutQuint); TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint); } + + #region IFilterable + + public abstract IEnumerable FilterTerms { get; } + + private bool matchingFilter; + + public virtual bool MatchingFilter + { + get => matchingFilter; + set + { + if (matchingFilter == value) + return; + + matchingFilter = value; + this.FadeTo(value ? 1 : 0); + } + } + + public bool FilteringActive { set { } } + + #endregion } } diff --git a/osu.Game/Overlays/Mods/ModState.cs b/osu.Game/Overlays/Mods/ModState.cs index 3ee890e876..35b264fe71 100644 --- a/osu.Game/Overlays/Mods/ModState.cs +++ b/osu.Game/Overlays/Mods/ModState.cs @@ -32,9 +32,16 @@ namespace osu.Game.Overlays.Mods public bool PendingConfiguration { get; set; } /// - /// Whether the mod is currently filtered out due to not matching imposed criteria. + /// Whether the mod is currently valid for selection. + /// This can be in scenarios such as the free mod select overlay, where not all mods are selectable + /// regardless of search criteria imposed by the user selecting. /// - public BindableBool Filtered { get; } = new BindableBool(); + public BindableBool ValidForSelection { get; } = new BindableBool(true); + + /// + /// Whether the mod is matching the current filter, i.e. it is available for user selection. + /// + public BindableBool MatchingFilter { get; } = new BindableBool(true); public ModState(Mod mod) { diff --git a/osu.Game/Overlays/Mods/SelectAllModsButton.cs b/osu.Game/Overlays/Mods/SelectAllModsButton.cs index f4b8025227..212563de7d 100644 --- a/osu.Game/Overlays/Mods/SelectAllModsButton.cs +++ b/osu.Game/Overlays/Mods/SelectAllModsButton.cs @@ -44,7 +44,7 @@ namespace osu.Game.Overlays.Mods { Enabled.Value = availableMods.Value .SelectMany(pair => pair.Value) - .Any(modState => !modState.Active.Value && !modState.Filtered.Value); + .Any(modState => !modState.Active.Value && !modState.MatchingFilter.Value); } public bool OnPressed(KeyBindingPressEvent e) From f0d35eb12be0262e892b4fc614128860007107c7 Mon Sep 17 00:00:00 2001 From: Cootz Date: Wed, 3 May 2023 13:12:11 +0300 Subject: [PATCH 0036/2100] Update testing --- .../TestSceneMultiplayerMatchSongSelect.cs | 2 +- .../TestSceneMultiplayerMatchSubScreen.cs | 2 +- .../Visual/UserInterface/TestSceneModColumn.cs | 12 ++++++------ .../UserInterface/TestSceneModSelectOverlay.cs | 12 ++++++------ 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index c0b6a0beab..dd1400b36e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -108,7 +108,7 @@ namespace osu.Game.Tests.Visual.Multiplayer () => this.ChildrenOfType() .Single() .ChildrenOfType() - .Where(panel => !panel.Filtered.Value) + .Where(panel => panel.MatchingFilter) .All(b => b.Mod.GetType() != type)); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 8816787ceb..8fd16fb723 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -203,7 +203,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("mod select contains only double time mod", () => this.ChildrenOfType().Single().UserModsSelectOverlay .ChildrenOfType() - .SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime); + .SingleOrDefault(panel => panel.MatchingFilter)?.Mod is OsuModDoubleTime); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index 3f5676ee24..ec6084aa8e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -106,26 +106,26 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase))); - AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => !panel.Filtered.Value) == 2); + AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => panel.MatchingFilter) == 2); clickToggle(); AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning); - AddAssert("only visible items selected", () => column.ChildrenOfType().Where(panel => panel.Active.Value).All(panel => !panel.Filtered.Value)); + AddAssert("only visible items selected", () => column.ChildrenOfType().Where(panel => panel.Active.Value).All(panel => panel.MatchingFilter)); AddStep("unset filter", () => setFilter(null)); - AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value)); + AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => panel.MatchingFilter)); AddAssert("checkbox not selected", () => !column.ChildrenOfType().Single().Current.Value); AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase))); - AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => !panel.Filtered.Value) == 2); + AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => panel.MatchingFilter) == 2); AddAssert("checkbox selected", () => column.ChildrenOfType().Single().Current.Value); AddStep("filter out everything", () => setFilter(_ => false)); - AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => panel.Filtered.Value)); + AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => !panel.MatchingFilter)); AddUntilStep("checkbox hidden", () => !column.ChildrenOfType().Single().IsPresent); AddStep("inset filter", () => setFilter(null)); - AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value)); + AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => panel.MatchingFilter)); AddUntilStep("checkbox visible", () => column.ChildrenOfType().Single().IsPresent); void clickToggle() => AddStep("click toggle", () => diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 5ccaebd721..9c6ec653e9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -431,15 +431,15 @@ namespace osu.Game.Tests.Visual.UserInterface createScreen(); changeRuleset(0); - AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value)); + AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.MatchingFilter)); AddStep("make double time invalid", () => modSelectOverlay.IsValidMod = m => !(m is OsuModDoubleTime)); - AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => panel.Filtered.Value)); - AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value)); + AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => !panel.MatchingFilter)); + AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModNightcore).Any(panel => panel.MatchingFilter)); AddStep("make double time valid again", () => modSelectOverlay.IsValidMod = _ => true); - AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value)); - AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(b => b.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value)); + AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.MatchingFilter)); + AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(b => b.Mod is OsuModNightcore).Any(panel => panel.MatchingFilter)); } [Test] @@ -465,7 +465,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo); waitForColumnLoad(); - AddAssert("unimplemented mod panel is filtered", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value); + AddAssert("unimplemented mod panel is filtered", () => !getPanelForMod(typeof(TestUnimplementedMod)).MatchingFilter); } [Test] From 152d2678d52c74d562c1c959da5502c960664ed1 Mon Sep 17 00:00:00 2001 From: Cootz Date: Wed, 3 May 2023 14:00:46 +0300 Subject: [PATCH 0037/2100] Fix `ModSelectColumn` completely disappear from `ModSelectOverlay` --- osu.Game/Overlays/Mods/ModSearch.cs | 10 -------- osu.Game/Overlays/Mods/ModSearchContainer.cs | 25 ++++++++++++++++++++ osu.Game/Overlays/Mods/ModSelectColumn.cs | 6 ++--- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- 4 files changed, 30 insertions(+), 15 deletions(-) delete mode 100644 osu.Game/Overlays/Mods/ModSearch.cs create mode 100644 osu.Game/Overlays/Mods/ModSearchContainer.cs diff --git a/osu.Game/Overlays/Mods/ModSearch.cs b/osu.Game/Overlays/Mods/ModSearch.cs deleted file mode 100644 index 0a82d967af..0000000000 --- a/osu.Game/Overlays/Mods/ModSearch.cs +++ /dev/null @@ -1,10 +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 osu.Framework.Graphics.Containers; - -namespace osu.Game.Overlays.Mods; - -public partial class ModSearch : Container -{ -} diff --git a/osu.Game/Overlays/Mods/ModSearchContainer.cs b/osu.Game/Overlays/Mods/ModSearchContainer.cs new file mode 100644 index 0000000000..29cc7d8132 --- /dev/null +++ b/osu.Game/Overlays/Mods/ModSearchContainer.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Overlays.Mods; + +public partial class ModSearchContainer : SearchContainer +{ + /// + /// A string that should match the children + /// + public string ForcedSearchTerm + { + get => SearchTerm; + set + { + if (value == SearchTerm) + return; + + SearchTerm = value; + Update(); + } + } +} diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index 8bf3fd404f..c80eb8c09c 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -45,13 +45,13 @@ namespace osu.Game.Overlays.Mods public string SearchTerm { - set => ItemsFlow.SearchTerm = value; + set => ItemsFlow.ForcedSearchTerm = value; } protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value; protected readonly Container ControlContainer; - protected readonly SearchContainer ItemsFlow; + protected readonly ModSearchContainer ItemsFlow; private readonly TextFlowContainer headerText; private readonly Box headerBackground; @@ -155,7 +155,7 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.Both, ClampExtension = 100, ScrollbarOverlapsContent = false, - Child = ItemsFlow = new SearchContainer + Child = ItemsFlow = new ModSearchContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 12e894cfba..9efcd24048 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -112,7 +112,7 @@ namespace osu.Game.Overlays.Mods private ShearedSearchTextBox searchTextBox = null!; private DifficultyMultiplierDisplay? multiplierDisplay; - private ModSearch? modSearch; + private ModSearchContainer? modSearch; protected ShearedButton BackButton { get; private set; } = null!; protected ShearedToggleButton? CustomisationButton { get; private set; } @@ -215,7 +215,7 @@ namespace osu.Game.Overlays.Mods AutoSizeAxes = Axes.X, Height = ModsEffectDisplay.HEIGHT, Margin = new MarginPadding { Horizontal = 100 }, - Child = modSearch = new ModSearch() + Child = modSearch = new ModSearchContainer() }); FooterContent.Child = footerButtonFlow = new FillFlowContainer From 1ac9d900e1f0ea8b66e5722174fc3f7f960b9e6b Mon Sep 17 00:00:00 2001 From: Cootz Date: Thu, 4 May 2023 19:24:37 +0300 Subject: [PATCH 0038/2100] Clear search on `ModSelectOverlay.Hide` --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 9efcd24048..fa39c2b09d 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -112,8 +112,6 @@ namespace osu.Game.Overlays.Mods private ShearedSearchTextBox searchTextBox = null!; private DifficultyMultiplierDisplay? multiplierDisplay; - private ModSearchContainer? modSearch; - protected ShearedButton BackButton { get; private set; } = null!; protected ShearedToggleButton? CustomisationButton { get; private set; } @@ -215,7 +213,7 @@ namespace osu.Game.Overlays.Mods AutoSizeAxes = Axes.X, Height = ModsEffectDisplay.HEIGHT, Margin = new MarginPadding { Horizontal = 100 }, - Child = modSearch = new ModSearchContainer() + Child = new ModSearchContainer() }); FooterContent.Child = footerButtonFlow = new FillFlowContainer @@ -243,6 +241,14 @@ namespace osu.Game.Overlays.Mods globalAvailableMods.BindTo(game.AvailableMods); } + public override void Hide() + { + base.Hide(); + + //We want to clear search for next user iteraction with mod overlay + searchTextBox.Current.Value = string.Empty; + } + private ModSettingChangeTracker? modSettingChangeTracker; protected override void LoadComplete() From 54757df51fefc2c8c3a19b7dc340036ceb5b6a29 Mon Sep 17 00:00:00 2001 From: Cootz Date: Fri, 5 May 2023 09:31:27 +0300 Subject: [PATCH 0039/2100] Fix mod deselect button not working properly when search applied --- osu.Game/Overlays/Mods/ModColumn.cs | 6 +++++- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 10 ---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index df2a7780d3..5da57ee4ed 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -206,8 +206,12 @@ namespace osu.Game.Overlays.Mods { pendingSelectionOperations.Clear(); - foreach (var button in availableMods.Where(b => b.Active.Value && b.MatchingFilter.Value)) + foreach (var button in availableMods.Where(b => b.Active.Value)) pendingSelectionOperations.Enqueue(() => button.Active.Value = false); + + //If column is hidden trigger selection manually + if (Alpha == 0f) + Update(); } /// diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index fa39c2b09d..d2af305d68 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -206,16 +206,6 @@ namespace osu.Game.Overlays.Mods }); } - MainAreaContent.Add(new Container - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.X, - Height = ModsEffectDisplay.HEIGHT, - Margin = new MarginPadding { Horizontal = 100 }, - Child = new ModSearchContainer() - }); - FooterContent.Child = footerButtonFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, From ab94b4dce194c85942bfa94d6c1f7bb16073dc93 Mon Sep 17 00:00:00 2001 From: Cootz Date: Fri, 5 May 2023 09:52:09 +0300 Subject: [PATCH 0040/2100] Update `ModPresetPanel.FilterTerms` to account mod names and acronyms --- osu.Game/Overlays/Mods/ModPresetPanel.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index 5b6146e106..5bc16abcab 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -83,11 +83,20 @@ namespace osu.Game.Overlays.Mods #region Filtering support - public override IEnumerable FilterTerms => new LocalisableString[] + public override IEnumerable FilterTerms => getFilterTerms(); + + private IEnumerable getFilterTerms() { - Preset.Value.Name, - Preset.Value.Description - }; + yield return Preset.Value.Name; + yield return Preset.Value.Description; + + foreach (Mod mod in Preset.Value.Mods) + { + yield return mod.Name; + yield return mod.Acronym; + yield return mod.Description; + } + } #endregion From 7422b5285ce8e9631111c20ab9a1baf40d81152a Mon Sep 17 00:00:00 2001 From: Cootz Date: Fri, 5 May 2023 22:41:30 +0300 Subject: [PATCH 0041/2100] Fix wrong filtering in testing --- .../UserInterface/TestSceneModColumn.cs | 2 +- .../TestSceneModSelectOverlay.cs | 12 ++++---- osu.Game/Overlays/Mods/ModColumn.cs | 3 +- osu.Game/Overlays/Mods/ModPanel.cs | 30 +++++++++++++++++-- osu.Game/Overlays/Mods/ModState.cs | 5 ++++ osu.Game/Overlays/Mods/SelectAllModsButton.cs | 2 +- 6 files changed, 43 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index ec6084aa8e..2ae95a3a09 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -291,7 +291,7 @@ namespace osu.Game.Tests.Visual.UserInterface private void setFilter(Func? filter) { foreach (var modState in this.ChildrenOfType().Single().AvailableMods) - modState.MatchingFilter.Value = filter?.Invoke(modState.Mod) == false; + modState.MatchingFilter.Value = filter?.Invoke(modState.Mod) ?? true; } private partial class TestModColumn : ModColumn diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 9c6ec653e9..006a6abbc2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -431,15 +431,15 @@ namespace osu.Game.Tests.Visual.UserInterface createScreen(); changeRuleset(0); - AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.MatchingFilter)); + AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.IsValid)); AddStep("make double time invalid", () => modSelectOverlay.IsValidMod = m => !(m is OsuModDoubleTime)); - AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => !panel.MatchingFilter)); - AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModNightcore).Any(panel => panel.MatchingFilter)); + AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => !panel.IsValid)); + AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModNightcore).Any(panel => panel.IsValid)); AddStep("make double time valid again", () => modSelectOverlay.IsValidMod = _ => true); - AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.MatchingFilter)); - AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(b => b.Mod is OsuModNightcore).Any(panel => panel.MatchingFilter)); + AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.IsValid)); + AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(b => b.Mod is OsuModNightcore).Any(panel => panel.IsValid)); } [Test] @@ -465,7 +465,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo); waitForColumnLoad(); - AddAssert("unimplemented mod panel is filtered", () => !getPanelForMod(typeof(TestUnimplementedMod)).MatchingFilter); + AddAssert("unimplemented mod panel is filtered", () => !getPanelForMod(typeof(TestUnimplementedMod)).IsValid); } [Test] diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 5da57ee4ed..c8822d78fd 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -47,6 +47,7 @@ namespace osu.Game.Overlays.Mods { mod.Active.BindValueChanged(_ => updateState()); mod.MatchingFilter.BindValueChanged(_ => updateState()); + mod.ValidForSelection.BindValueChanged(_ => updateState()); } updateState(); @@ -145,7 +146,7 @@ namespace osu.Game.Overlays.Mods private void updateState() { - Alpha = availableMods.All(mod => !mod.MatchingFilter.Value) ? 0 : 1; + Alpha = availableMods.All(mod => !mod.MatchingFilter.Value || !mod.ValidForSelection.Value) ? 0 : 1; if (toggleAllCheckbox != null && !SelectionAnimationRunning) { diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index a5ff52dfd2..cd94226d8f 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -56,7 +56,8 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); - canBeShown.BindTo(modState.ValidForSelection); + modState.ValidForSelection.BindValueChanged(_ => updateFilterState()); + modState.MatchingFilter.BindValueChanged(_ => updateFilterState(), true); } protected override void Select() @@ -71,6 +72,23 @@ namespace osu.Game.Overlays.Mods Active.Value = false; } + /// + /// Determine if is valid and can be shown + /// + public bool IsValid => modState.IsValid; + + public bool ValidForSelection + { + get => modState.ValidForSelection.Value; + set + { + if (modState.ValidForSelection.Value == value) + return; + + modState.ValidForSelection.Value = value; + } + } + #region Filtering support public override IEnumerable FilterTerms => new[] @@ -89,13 +107,21 @@ namespace osu.Game.Overlays.Mods return; modState.MatchingFilter.Value = value; - this.FadeTo(value ? 1 : 0); } } + /// + /// This property is always because it affects search result + /// private readonly BindableBool canBeShown = new BindableBool(true); + IBindable IConditionalFilterable.CanBeShown => canBeShown; + private void updateFilterState() + { + this.FadeTo(IsValid ? 1 : 0); + } + #endregion } } diff --git a/osu.Game/Overlays/Mods/ModState.cs b/osu.Game/Overlays/Mods/ModState.cs index 35b264fe71..be770ec937 100644 --- a/osu.Game/Overlays/Mods/ModState.cs +++ b/osu.Game/Overlays/Mods/ModState.cs @@ -38,6 +38,11 @@ namespace osu.Game.Overlays.Mods /// public BindableBool ValidForSelection { get; } = new BindableBool(true); + /// + /// Determine if is valid and can be shown + /// + public bool IsValid => MatchingFilter.Value && ValidForSelection.Value; + /// /// Whether the mod is matching the current filter, i.e. it is available for user selection. /// diff --git a/osu.Game/Overlays/Mods/SelectAllModsButton.cs b/osu.Game/Overlays/Mods/SelectAllModsButton.cs index 212563de7d..dad4f7b629 100644 --- a/osu.Game/Overlays/Mods/SelectAllModsButton.cs +++ b/osu.Game/Overlays/Mods/SelectAllModsButton.cs @@ -44,7 +44,7 @@ namespace osu.Game.Overlays.Mods { Enabled.Value = availableMods.Value .SelectMany(pair => pair.Value) - .Any(modState => !modState.Active.Value && !modState.MatchingFilter.Value); + .Any(modState => !modState.Active.Value && modState.IsValid); } public bool OnPressed(KeyBindingPressEvent e) From a226caff560afc897e3c596e08a0fcfa74b6a3a0 Mon Sep 17 00:00:00 2001 From: Cootz Date: Sat, 6 May 2023 11:09:44 +0300 Subject: [PATCH 0042/2100] Fix testing --- .../TestSceneMultiplayerMatchSubScreen.cs | 2 +- .../Visual/UserInterface/TestSceneModColumn.cs | 14 +++++++------- .../Overlays/Mods/Input/ClassicModHotkeyHandler.cs | 2 +- .../Mods/Input/SequentialModHotkeyHandler.cs | 2 +- osu.Game/Overlays/Mods/ModColumn.cs | 8 ++++---- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 8fd16fb723..40acea475b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -203,7 +203,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("mod select contains only double time mod", () => this.ChildrenOfType().Single().UserModsSelectOverlay .ChildrenOfType() - .SingleOrDefault(panel => panel.MatchingFilter)?.Mod is OsuModDoubleTime); + .SingleOrDefault(panel => panel.IsValid)?.Mod is OsuModDoubleTime); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index 2ae95a3a09..fc1db3c644 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -106,26 +106,26 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase))); - AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => panel.MatchingFilter) == 2); + AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => panel.IsValid) == 2); clickToggle(); AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning); - AddAssert("only visible items selected", () => column.ChildrenOfType().Where(panel => panel.Active.Value).All(panel => panel.MatchingFilter)); + AddAssert("only visible items selected", () => column.ChildrenOfType().Where(panel => panel.Active.Value).All(panel => panel.IsValid)); AddStep("unset filter", () => setFilter(null)); - AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => panel.MatchingFilter)); + AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => panel.IsValid)); AddAssert("checkbox not selected", () => !column.ChildrenOfType().Single().Current.Value); AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase))); - AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => panel.MatchingFilter) == 2); + AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => panel.IsValid) == 2); AddAssert("checkbox selected", () => column.ChildrenOfType().Single().Current.Value); AddStep("filter out everything", () => setFilter(_ => false)); - AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => !panel.MatchingFilter)); + AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => !panel.IsValid)); AddUntilStep("checkbox hidden", () => !column.ChildrenOfType().Single().IsPresent); AddStep("inset filter", () => setFilter(null)); - AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => panel.MatchingFilter)); + AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => panel.IsValid)); AddUntilStep("checkbox visible", () => column.ChildrenOfType().Single().IsPresent); void clickToggle() => AddStep("click toggle", () => @@ -291,7 +291,7 @@ namespace osu.Game.Tests.Visual.UserInterface private void setFilter(Func? filter) { foreach (var modState in this.ChildrenOfType().Single().AvailableMods) - modState.MatchingFilter.Value = filter?.Invoke(modState.Mod) ?? true; + modState.ValidForSelection.Value = filter?.Invoke(modState.Mod) ?? true; } private partial class TestModColumn : ModColumn diff --git a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs index 566e741880..343f7242f1 100644 --- a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs +++ b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs @@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Mods.Input if (!mod_type_lookup.TryGetValue(e.Key, out var typesToMatch)) return false; - var matchingMods = availableMods.Where(modState => matches(modState, typesToMatch) && modState.MatchingFilter.Value).ToArray(); + var matchingMods = availableMods.Where(modState => matches(modState, typesToMatch) && modState.IsValid).ToArray(); if (matchingMods.Length == 0) return false; diff --git a/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs index b4ec8ad4c8..cbfa96307d 100644 --- a/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs +++ b/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Mods.Input if (index < 0) return false; - var modState = availableMods.Where(modState => modState.MatchingFilter.Value).ElementAtOrDefault(index); + var modState = availableMods.Where(modState => modState.IsValid).ElementAtOrDefault(index); if (modState == null) return false; diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index c8822d78fd..b53e621759 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -146,12 +146,12 @@ namespace osu.Game.Overlays.Mods private void updateState() { - Alpha = availableMods.All(mod => !mod.MatchingFilter.Value || !mod.ValidForSelection.Value) ? 0 : 1; + Alpha = availableMods.All(mod => !mod.IsValid) ? 0 : 1; if (toggleAllCheckbox != null && !SelectionAnimationRunning) { - toggleAllCheckbox.Alpha = availableMods.Any(panel => panel.MatchingFilter.Value) ? 1 : 0; - toggleAllCheckbox.Current.Value = availableMods.Where(panel => panel.MatchingFilter.Value).All(panel => panel.Active.Value); + toggleAllCheckbox.Alpha = availableMods.Any(panel => panel.IsValid) ? 1 : 0; + toggleAllCheckbox.Current.Value = availableMods.Where(panel => panel.IsValid).All(panel => panel.Active.Value); } } @@ -196,7 +196,7 @@ namespace osu.Game.Overlays.Mods { pendingSelectionOperations.Clear(); - foreach (var button in availableMods.Where(b => !b.Active.Value && b.MatchingFilter.Value)) + foreach (var button in availableMods.Where(b => !b.Active.Value && b.IsValid)) pendingSelectionOperations.Enqueue(() => button.Active.Value = true); } From cbb9f0100e2dad808ae7309d74b8f3bb701aa5d1 Mon Sep 17 00:00:00 2001 From: Cootz Date: Sat, 6 May 2023 11:27:06 +0300 Subject: [PATCH 0043/2100] Update `PopIn` and `PopOut` filtering. Expose `SearchTerm` --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index d2af305d68..cb8eddca62 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -64,6 +64,21 @@ namespace osu.Game.Overlays.Mods } } + /// + /// Search term applied on mod overlay + /// + public string SearchTerm + { + get => searchTextBox.Current.Value; + set + { + if (searchTextBox.Current.Value == value) + return; + + searchTextBox.Current.Value = value; + } + } + /// /// Whether the total score multiplier calculated from the current selected set of mods should be shown. /// @@ -494,7 +509,7 @@ namespace osu.Game.Overlays.Mods { var column = columnFlow[i].Column; - bool allFiltered = column is ModColumn modColumn && modColumn.AvailableMods.All(modState => !modState.MatchingFilter.Value); + bool allFiltered = column is ModColumn modColumn && modColumn.AvailableMods.All(modState => !modState.IsValid); double delay = allFiltered ? 0 : nonFilteredColumnCount * 30; double duration = allFiltered ? 0 : fade_in_duration; @@ -556,7 +571,7 @@ namespace osu.Game.Overlays.Mods if (column is ModColumn modColumn) { - allFiltered = modColumn.AvailableMods.All(modState => !modState.MatchingFilter.Value); + allFiltered = modColumn.AvailableMods.All(modState => !modState.IsValid); modColumn.FlushPendingSelections(); } From 5aca3a78dac91fb940d3ccdf82e077db374f708a Mon Sep 17 00:00:00 2001 From: Cootz Date: Sat, 6 May 2023 12:21:32 +0300 Subject: [PATCH 0044/2100] Add basic tests for external search --- .../UserInterface/TestSceneModColumn.cs | 43 ++++++++++++ .../TestSceneModSelectOverlay.cs | 65 +++++++++++++++++-- 2 files changed, 101 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index fc1db3c644..394a38fe5d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -288,6 +288,49 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("no change", () => this.ChildrenOfType().Count(panel => panel.Active.Value) == 2); } + [Test] + public void TestApplySearchTerms() + { + Mod hidden = getExampleModsFor(ModType.DifficultyIncrease).Where(modState => modState.Mod is ModHidden).Select(modState => modState.Mod).Single(); + + ModColumn column = null!; + AddStep("create content", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(30), + Child = column = new ModColumn(ModType.DifficultyIncrease, false) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AvailableMods = getExampleModsFor(ModType.DifficultyIncrease) + } + }); + + applySearchAndAssert(hidden.Name); + + clearSearch(); + + applySearchAndAssert(hidden.Acronym); + + clearSearch(); + + applySearchAndAssert(hidden.Description.ToString()); + + void applySearchAndAssert(string searchTerm) + { + AddStep("search by mod name", () => column.SearchTerm = searchTerm); + + AddAssert("only hidden is visible", () => column.ChildrenOfType().Where(panel => panel.IsValid).All(panel => panel.Mod is ModHidden)); + } + + void clearSearch() + { + AddStep("clear search", () => column.SearchTerm = string.Empty); + + AddAssert("all mods are visible", () => column.ChildrenOfType().All(panel => panel.IsValid)); + } + } + private void setFilter(Func? filter) { foreach (var modState in this.ChildrenOfType().Single().AvailableMods) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 006a6abbc2..e8a835a330 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -521,8 +521,11 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden); } + /// + /// Internal search applies from code by setting + /// [Test] - public void TestColumnHiding() + public void TestColumnHidingOnInternalSearch() { AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay { @@ -551,6 +554,56 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("3 columns visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 3); } + /// + /// External search applies by user by entering search term into search bar + /// + [Test] + public void TestColumnHidingOnExternalSearch() + { + AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + SelectedMods = { BindTarget = SelectedMods } + }); + waitForColumnLoad(); + changeRuleset(0); + + AddAssert("all columns visible", () => this.ChildrenOfType().All(col => col.IsPresent)); + + AddStep("set search", () => modSelectOverlay.SearchTerm = "HD"); + AddAssert("one column visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 1); + + AddStep("filter out everything", () => modSelectOverlay.SearchTerm = "Some long search term with no matches"); + AddAssert("no columns visible", () => this.ChildrenOfType().All(col => !col.IsPresent)); + + AddStep("clear search bar", () => modSelectOverlay.SearchTerm = ""); + AddAssert("all columns visible", () => this.ChildrenOfType().All(col => col.IsPresent)); + } + + [Test] + public void TestHidingOverlayClearsSearch() + { + AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + SelectedMods = { BindTarget = SelectedMods } + }); + waitForColumnLoad(); + changeRuleset(0); + + AddAssert("all columns visible", () => this.ChildrenOfType().All(col => col.IsPresent)); + + AddStep("set search", () => modSelectOverlay.SearchTerm = "fail"); + AddAssert("one column visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 2); + + AddStep("hide", () => modSelectOverlay.Hide()); + AddStep("show", () => modSelectOverlay.Show()); + + AddAssert("all columns visible", () => this.ChildrenOfType().All(col => col.IsPresent)); + } + [Test] public void TestColumnHidingOnRulesetChange() { @@ -605,12 +658,10 @@ namespace osu.Game.Tests.Visual.UserInterface { public override string ShortName => "unimplemented"; - public override IEnumerable GetModsFor(ModType type) - { - if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() }); - - return base.GetModsFor(type); - } + public override IEnumerable GetModsFor(ModType type) => + type == ModType.Conversion + ? base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() }) + : base.GetModsFor(type); } } } From 4d235105d14d0b1ed91fedae09c6eaea6bdf13bf Mon Sep 17 00:00:00 2001 From: Cootz Date: Sun, 7 May 2023 15:14:49 +0300 Subject: [PATCH 0045/2100] Convert `ModSearchContainer` to block-scoped namespace --- osu.Game/Overlays/Mods/ModSearchContainer.cs | 27 ++++++++++---------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSearchContainer.cs b/osu.Game/Overlays/Mods/ModSearchContainer.cs index 29cc7d8132..f9bf11d1f1 100644 --- a/osu.Game/Overlays/Mods/ModSearchContainer.cs +++ b/osu.Game/Overlays/Mods/ModSearchContainer.cs @@ -3,23 +3,24 @@ using osu.Framework.Graphics.Containers; -namespace osu.Game.Overlays.Mods; - -public partial class ModSearchContainer : SearchContainer +namespace osu.Game.Overlays.Mods { - /// - /// A string that should match the children - /// - public string ForcedSearchTerm + public partial class ModSearchContainer : SearchContainer { - get => SearchTerm; - set + /// + /// A string that should match the children + /// + public string ForcedSearchTerm { - if (value == SearchTerm) - return; + get => SearchTerm; + set + { + if (value == SearchTerm) + return; - SearchTerm = value; - Update(); + SearchTerm = value; + Update(); + } } } } From 60bad35145ac63e0d357d801c4a6fcf2a56a5812 Mon Sep 17 00:00:00 2001 From: Cootz Date: Sun, 7 May 2023 15:34:01 +0300 Subject: [PATCH 0046/2100] Remove weird update usage when 'deselect all' pressed --- osu.Game/Overlays/Mods/ModColumn.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index b53e621759..71d964c618 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -208,11 +208,12 @@ namespace osu.Game.Overlays.Mods pendingSelectionOperations.Clear(); foreach (var button in availableMods.Where(b => b.Active.Value)) - pendingSelectionOperations.Enqueue(() => button.Active.Value = false); - - //If column is hidden trigger selection manually - if (Alpha == 0f) - Update(); + { + if (Alpha == 0f) + button.Active.Value = false; //If column is hidden change state manually without any animation + else + pendingSelectionOperations.Enqueue(() => button.Active.Value = false); + } } /// From 4c3af6ecfed1f17dcf3f60193247c3394a4f152c Mon Sep 17 00:00:00 2001 From: Cootz Date: Sun, 7 May 2023 15:50:21 +0300 Subject: [PATCH 0047/2100] Add test coverage for deselect all with filtered mods selected --- .../TestSceneModSelectOverlay.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index e8a835a330..8e4f437f44 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -502,6 +502,31 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("deselect all button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); } + [Test] + public void TestDeselectAllViaButton_WithSearchApplied() + { + createScreen(); + changeRuleset(0); + + AddAssert("deselect all button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + + AddStep("select DT + HD + RD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModRandom() }); + AddAssert("DT + HD + RD selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 3); + AddAssert("deselect all button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + + AddStep("apply search", () => modSelectOverlay.SearchTerm = "Easy"); + AddAssert("DT + HD + RD are hidden and selected", () => modSelectOverlay.ChildrenOfType().Count(panel => !panel.IsValid && panel.Active.Value) == 3); + AddAssert("deselect all button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + + AddStep("click deselect all button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any()); + AddAssert("deselect all button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + } + [Test] public void TestCloseViaBackButton() { From 2467813d8199473f2b37041fb005065c312dd7d2 Mon Sep 17 00:00:00 2001 From: Cootz Date: Tue, 9 May 2023 16:14:42 +0300 Subject: [PATCH 0048/2100] Block `deselect all` short key when using the search box --- .../TestSceneModSelectOverlay.cs | 20 +++++++++++++++++++ .../UserInterface/ShearedSearchTextBox.cs | 2 ++ .../Overlays/Mods/DeselectAllModsButton.cs | 8 +++++--- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 20 +++++++++++-------- 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 8e4f437f44..d1cbe7d91c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -481,6 +481,26 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any()); } + [Test] + public void TestDeselectAllViaKey_WithSearchApplied() + { + createScreen(); + changeRuleset(0); + + AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() }); + AddStep("focus on search", () => modSelectOverlay.SearchTextBox.TakeFocus()); + AddStep("apply search", () => modSelectOverlay.SearchTerm = "Easy"); + AddAssert("DT + HD selected and hidden", () => modSelectOverlay.ChildrenOfType().Count(panel => !panel.IsValid && panel.Active.Value) == 2); + + AddStep("press backspace", () => InputManager.Key(Key.BackSpace)); + AddAssert("DT + HD still selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 2); + AddAssert("search term changed", () => modSelectOverlay.SearchTerm == "Eas"); + + AddStep("kill focus", () => modSelectOverlay.SearchTextBox.KillFocus()); + AddStep("press backspace", () => InputManager.Key(Key.BackSpace)); + AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any()); + } + [Test] public void TestDeselectAllViaButton() { diff --git a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs index 7bd083f9d5..a6954fafb1 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs @@ -37,6 +37,8 @@ namespace osu.Game.Graphics.UserInterface set => textBox.HoldFocus = value; } + public new bool HasFocus => textBox.HasFocus; + public void TakeFocus() => textBox.TakeFocus(); public void KillFocus() => textBox.KillFocus(); diff --git a/osu.Game/Overlays/Mods/DeselectAllModsButton.cs b/osu.Game/Overlays/Mods/DeselectAllModsButton.cs index 3e5a3b12d1..d4d196508f 100644 --- a/osu.Game/Overlays/Mods/DeselectAllModsButton.cs +++ b/osu.Game/Overlays/Mods/DeselectAllModsButton.cs @@ -18,6 +18,7 @@ namespace osu.Game.Overlays.Mods public partial class DeselectAllModsButton : ShearedButton, IKeyBindingHandler { private readonly Bindable> selectedMods = new Bindable>(); + private readonly ShearedSearchTextBox searchTextBox; public DeselectAllModsButton(ModSelectOverlay modSelectOverlay) : base(ModSelectOverlay.BUTTON_WIDTH) @@ -25,6 +26,8 @@ namespace osu.Game.Overlays.Mods Text = CommonStrings.DeselectAll; Action = modSelectOverlay.DeselectAll; + searchTextBox = modSelectOverlay.SearchTextBox; + selectedMods.BindTo(modSelectOverlay.SelectedMods); } @@ -40,9 +43,8 @@ namespace osu.Game.Overlays.Mods Enabled.Value = selectedMods.Value.Any(); } - public bool OnPressed(KeyBindingPressEvent e) - { - if (e.Repeat || e.Action != GlobalAction.DeselectAllMods) + public bool OnPressed(KeyBindingPressEvent e) { + if (e.Repeat || e.Action != GlobalAction.DeselectAllMods || searchTextBox.HasFocus) return false; TriggerClick(); diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index cb8eddca62..fdc948bcbf 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -69,16 +69,21 @@ namespace osu.Game.Overlays.Mods /// public string SearchTerm { - get => searchTextBox.Current.Value; + get => SearchTextBox.Current.Value; set { - if (searchTextBox.Current.Value == value) + if (SearchTextBox.Current.Value == value) return; - searchTextBox.Current.Value = value; + SearchTextBox.Current.Value = value; } } + /// + /// Search box applied on mod overlay + /// + public ShearedSearchTextBox SearchTextBox { get; private set; } = null!; + /// /// Whether the total score multiplier calculated from the current selected set of mods should be shown. /// @@ -124,7 +129,6 @@ namespace osu.Game.Overlays.Mods private FillFlowContainer footerButtonFlow = null!; private Container aboveColumnsContent = null!; - private ShearedSearchTextBox searchTextBox = null!; private DifficultyMultiplierDisplay? multiplierDisplay; protected ShearedButton BackButton { get; private set; } = null!; @@ -168,7 +172,7 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.X, Height = ModsEffectDisplay.HEIGHT, Padding = new MarginPadding { Horizontal = 100 }, - Child = searchTextBox = new ShearedSearchTextBox + Child = SearchTextBox = new ShearedSearchTextBox { HoldFocus = false, Width = 300 @@ -250,8 +254,8 @@ namespace osu.Game.Overlays.Mods { base.Hide(); - //We want to clear search for next user iteraction with mod overlay - searchTextBox.Current.Value = string.Empty; + //We want to clear search for next user interaction with mod overlay + SearchTextBox.Current.Value = string.Empty; } private ModSettingChangeTracker? modSettingChangeTracker; @@ -287,7 +291,7 @@ namespace osu.Game.Overlays.Mods customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); - searchTextBox.Current.BindValueChanged(query => + SearchTextBox.Current.BindValueChanged(query => { foreach (var column in columnFlow.Columns) column.SearchTerm = query.NewValue; From ca688507304721a799c85a40900c1624976954c7 Mon Sep 17 00:00:00 2001 From: Cootz Date: Wed, 10 May 2023 19:43:58 +0300 Subject: [PATCH 0049/2100] fix formatting --- osu.Game/Overlays/Mods/DeselectAllModsButton.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/DeselectAllModsButton.cs b/osu.Game/Overlays/Mods/DeselectAllModsButton.cs index d4d196508f..bdb37e3ead 100644 --- a/osu.Game/Overlays/Mods/DeselectAllModsButton.cs +++ b/osu.Game/Overlays/Mods/DeselectAllModsButton.cs @@ -43,7 +43,8 @@ namespace osu.Game.Overlays.Mods Enabled.Value = selectedMods.Value.Any(); } - public bool OnPressed(KeyBindingPressEvent e) { + public bool OnPressed(KeyBindingPressEvent e) + { if (e.Repeat || e.Action != GlobalAction.DeselectAllMods || searchTextBox.HasFocus) return false; From e5884016ab7ad72fda9be0878afec14bb8363b29 Mon Sep 17 00:00:00 2001 From: John Date: Fri, 12 May 2023 19:07:25 -0700 Subject: [PATCH 0050/2100] Initial commit for the snap colour mod. Implements basic functionality. --- .../Mods/OsuModSnapColour.cs | 42 +++++++++++++++++++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 1 + osu.Game/Rulesets/Mods/ModSnapColour.cs | 19 +++++++++ 3 files changed, 62 insertions(+) create mode 100644 osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs create mode 100644 osu.Game/Rulesets/Mods/ModSnapColour.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs new file mode 100644 index 0000000000..87a2598695 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. +#nullable disable + +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Screens.Edit; + +namespace osu.Game.Rulesets.Osu.Mods +{ + /// + /// Mod that colours s based on the musical division they are on + /// + public class OsuModSnapColour : ModSnapColour, IApplicableToBeatmap, IApplicableToDrawableHitObject + { + [Resolved] + private OsuColour colours { get; set; } = new OsuColour(); + + [Resolved(canBeNull: true)] + private IBeatmap currentBeatmap { get; set; } + + public void ApplyToBeatmap(IBeatmap beatmap) + { + //Store a reference to the current beatmap to look up the beat divisor when notes are drawn + if (this.currentBeatmap != beatmap) + this.currentBeatmap = beatmap; + } + + public void ApplyToDrawableHitObject(DrawableHitObject drawable) + { + if (currentBeatmap.IsNull() || drawable.IsNull()) return; + + drawable.OnUpdate += _ => + drawable.AccentColour.Value = BindableBeatDivisor.GetColourFor(currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(drawable.HitObject.StartTime), colours); + } + } +} diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 922594a93a..23eea0e488 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -176,6 +176,7 @@ namespace osu.Game.Rulesets.Osu new OsuModClassic(), new OsuModRandom(), new OsuModMirror(), + new OsuModSnapColour(), new MultiMod(new OsuModAlternate(), new OsuModSingleTap()) }; diff --git a/osu.Game/Rulesets/Mods/ModSnapColour.cs b/osu.Game/Rulesets/Mods/ModSnapColour.cs new file mode 100644 index 0000000000..6f706f1cb2 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModSnapColour.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// Mod that colours hitobjects based on the musical division they are on + /// + public class ModSnapColour : Mod + { + public override string Name => "Snap Colour"; + public override string Acronym => "SC"; + public override LocalisableString Description => new LocalisableString("Colours hit objects based on the rhythm."); + public override double ScoreMultiplier => 1; + public override ModType Type => ModType.Conversion; + } +} From 24f07633f36e965532c15e2f2ca53699dbc481eb Mon Sep 17 00:00:00 2001 From: John Date: Fri, 12 May 2023 19:43:28 -0700 Subject: [PATCH 0051/2100] Formatting fixes --- osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs index 87a2598695..556af7e6b4 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.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. + #nullable disable using osu.Framework.Allocation; @@ -36,7 +37,9 @@ namespace osu.Game.Rulesets.Osu.Mods if (currentBeatmap.IsNull() || drawable.IsNull()) return; drawable.OnUpdate += _ => - drawable.AccentColour.Value = BindableBeatDivisor.GetColourFor(currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(drawable.HitObject.StartTime), colours); + drawable.AccentColour.Value = BindableBeatDivisor.GetColourFor( + currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(drawable.HitObject.StartTime), + colours); } } } From 7a907f72070fd7248ca192c8918ed589da2bf2e6 Mon Sep 17 00:00:00 2001 From: John Date: Fri, 12 May 2023 22:13:39 -0700 Subject: [PATCH 0052/2100] Code quality improvements (thanks to ItsShamed): Removed #nullable disable, fixed incorrect LocalisableString, removed incorrect dependency injection for OsuColour, fixed nullable dependency for IBeatmap Removed unnecessary usage of "this." caught by the CI code quality check --- osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs | 13 +++++-------- osu.Game/Rulesets/Mods/ModSnapColour.cs | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs index 556af7e6b4..69b8b1d4c3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Beatmaps; @@ -19,17 +17,16 @@ namespace osu.Game.Rulesets.Osu.Mods /// public class OsuModSnapColour : ModSnapColour, IApplicableToBeatmap, IApplicableToDrawableHitObject { - [Resolved] - private OsuColour colours { get; set; } = new OsuColour(); + private readonly OsuColour colours = new OsuColour(); - [Resolved(canBeNull: true)] - private IBeatmap currentBeatmap { get; set; } + [Resolved] + private IBeatmap? currentBeatmap { get; set; } public void ApplyToBeatmap(IBeatmap beatmap) { //Store a reference to the current beatmap to look up the beat divisor when notes are drawn - if (this.currentBeatmap != beatmap) - this.currentBeatmap = beatmap; + if (currentBeatmap != beatmap) + currentBeatmap = beatmap; } public void ApplyToDrawableHitObject(DrawableHitObject drawable) diff --git a/osu.Game/Rulesets/Mods/ModSnapColour.cs b/osu.Game/Rulesets/Mods/ModSnapColour.cs index 6f706f1cb2..d7e51d8cf6 100644 --- a/osu.Game/Rulesets/Mods/ModSnapColour.cs +++ b/osu.Game/Rulesets/Mods/ModSnapColour.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mods { public override string Name => "Snap Colour"; public override string Acronym => "SC"; - public override LocalisableString Description => new LocalisableString("Colours hit objects based on the rhythm."); + public override LocalisableString Description => "Colours hit objects based on the rhythm."; public override double ScoreMultiplier => 1; public override ModType Type => ModType.Conversion; } From 6647d95ea7e80e8430c3daa8cd0bae62c2844c42 Mon Sep 17 00:00:00 2001 From: Cootz Date: Sun, 14 May 2023 18:32:16 +0300 Subject: [PATCH 0053/2100] Kill search focus when clicking on `ModColumn` --- .../UserInterface/TestSceneModSelectOverlay.cs | 18 ++++++++++++++++++ osu.Game/Overlays/Mods/ModSelectOverlay.cs | 3 +++ 2 files changed, 21 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index d1cbe7d91c..ea81f9c96e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -468,6 +468,24 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("unimplemented mod panel is filtered", () => !getPanelForMod(typeof(TestUnimplementedMod)).IsValid); } + [Test] + public void TestSearchFocusChange() + { + createScreen(); + + AddStep("click on search", navigateAndClick); + AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus); + + AddStep("click on mod column", navigateAndClick); + AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus); + + void navigateAndClick() where T : Drawable + { + InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().FirstOrDefault()); + InputManager.Click(MouseButton.Left); + } + } + [Test] public void TestDeselectAllViaKey() { diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index fdc948bcbf..ace5bf71d1 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -777,6 +777,9 @@ namespace osu.Game.Overlays.Mods if (!Active.Value) RequestScroll?.Invoke(this); + //Kill focus on SearchTextBox + Scheduler.Add(() => GetContainingInputManager().ChangeFocus(null)); + return true; } From e2633ae993ccd0d470a5684b3cf39ea1c850e0dd Mon Sep 17 00:00:00 2001 From: John Date: Tue, 16 May 2023 21:08:56 -0700 Subject: [PATCH 0054/2100] Removed unnecessary [Resolved] attribute (thanks bdach) Moved accent color assignment from OnUpdate to ApplyCustomUpdateState. In order to get this to work, a flag needed to be added to DrawableHitObject.cs to disable combo color updates also being applied. --- osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs | 11 +++++++---- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 6 ++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs index 69b8b1d4c3..7d36e73cdb 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs @@ -33,10 +33,13 @@ namespace osu.Game.Rulesets.Osu.Mods { if (currentBeatmap.IsNull() || drawable.IsNull()) return; - drawable.OnUpdate += _ => - drawable.AccentColour.Value = BindableBeatDivisor.GetColourFor( - currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(drawable.HitObject.StartTime), - colours); + drawable.ApplyCustomUpdateState += (drawableObject, state) => + { + int snapDivisor = currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(drawableObject.HitObject.StartTime); + + drawableObject.EnableComboColour = false; + drawableObject.AccentColour.Value = BindableBeatDivisor.GetColourFor(snapDivisor, colours); + }; } } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 07c0d1f8a1..cbaa07bebc 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -74,6 +74,11 @@ namespace osu.Game.Rulesets.Objects.Drawables public override bool PropagateNonPositionalInputSubTree => HandleUserInput; + /// + /// Whether this object should be coloured using its combo position + /// + public bool EnableComboColour { get; set; } = true; + /// /// Invoked by this or a nested after a has been applied. /// @@ -528,6 +533,7 @@ namespace osu.Game.Rulesets.Objects.Drawables protected void UpdateComboColour() { if (!(HitObject is IHasComboInformation combo)) return; + if (!EnableComboColour) return; Color4 colour = combo.GetComboColour(CurrentSkin); From b7dc8d49bad8d28647bfae7f85a017d92b64a501 Mon Sep 17 00:00:00 2001 From: John Date: Tue, 16 May 2023 21:14:55 -0700 Subject: [PATCH 0055/2100] Removed import for Allocation and other unnecessary [Resolved] tag missed in last commit --- osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs index 7d36e73cdb..4fda6130c0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs @@ -1,7 +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 osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -19,7 +18,6 @@ namespace osu.Game.Rulesets.Osu.Mods { private readonly OsuColour colours = new OsuColour(); - [Resolved] private IBeatmap? currentBeatmap { get; set; } public void ApplyToBeatmap(IBeatmap beatmap) From 67bf1b4dfe5117cbf1e7a30b1a98caf271f8d637 Mon Sep 17 00:00:00 2001 From: Cootz Date: Sun, 21 May 2023 11:05:01 +0300 Subject: [PATCH 0056/2100] Select/deselect first visible mod when `GlobalAction.Select` is triggered --- .../UserInterface/TestSceneModSelectOverlay.cs | 18 ++++++++++++++++++ osu.Game/Overlays/Mods/ModSelectOverlay.cs | 18 ++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index ea81f9c96e..499a30f0dc 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -468,6 +468,24 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("unimplemented mod panel is filtered", () => !getPanelForMod(typeof(TestUnimplementedMod)).IsValid); } + [Test] + public void TestFirstModSelectDeselect() + { + createScreen(); + + AddStep("apply search", () => modSelectOverlay.SearchTerm = "HD"); + + AddStep("press enter", () => InputManager.Key(Key.Enter)); + AddAssert("hidden selected", () => getPanelForMod(typeof(OsuModHidden)).Active.Value); + + AddStep("press enter again", () => InputManager.Key(Key.Enter)); + AddAssert("hidden deselected", () => !getPanelForMod(typeof(OsuModHidden)).Active.Value); + + AddStep("clear search", () => modSelectOverlay.SearchTerm = string.Empty); + AddStep("press enter", () => InputManager.Key(Key.Enter)); + AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden); + } + [Test] public void TestSearchFocusChange() { diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index ace5bf71d1..eb0e797eac 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -612,10 +612,24 @@ namespace osu.Game.Overlays.Mods // This is handled locally here because this overlay is being registered at the game level // and therefore takes away keyboard focus from the screen stack. case GlobalAction.ToggleModSelection: + // Pressing toggle should completely hide the overlay in one shot. + hideOverlay(true); + return true; + case GlobalAction.Select: { - // Pressing toggle or select should completely hide the overlay in one shot. - hideOverlay(true); + // Pressing select should select first filtered mod or completely hide the overlay in one shot if search term is empty. + if (string.IsNullOrEmpty(SearchTerm)) + { + hideOverlay(true); + return true; + } + + ModState? firstMod = columnFlow.Columns.OfType().FirstOrDefault(m => m.IsPresent)?.AvailableMods.FirstOrDefault(x => x.IsValid); + + if (firstMod is not null) + firstMod.Active.Value = !firstMod.Active.Value; + return true; } } From 5ff023113fed2c6ef46454e62e9db2f5e8afcc3d Mon Sep 17 00:00:00 2001 From: Cootz Date: Sun, 28 May 2023 10:04:26 +0300 Subject: [PATCH 0057/2100] Update xmldoc for `ForcedSearchTerm` --- osu.Game/Overlays/Mods/ModSearchContainer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModSearchContainer.cs b/osu.Game/Overlays/Mods/ModSearchContainer.cs index f9bf11d1f1..b959274391 100644 --- a/osu.Game/Overlays/Mods/ModSearchContainer.cs +++ b/osu.Game/Overlays/Mods/ModSearchContainer.cs @@ -10,6 +10,9 @@ namespace osu.Game.Overlays.Mods /// /// A string that should match the children /// + /// + /// Same as except the filtering is guarantied to be performed even when can't be run. + /// public string ForcedSearchTerm { get => SearchTerm; From 0e5c99b760a8e3e2298f5bfb1ac2fb8d82ee83d2 Mon Sep 17 00:00:00 2001 From: Cootz Date: Sun, 28 May 2023 13:12:57 +0300 Subject: [PATCH 0058/2100] Fix search bar showing incorrectly --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index d3a2e001e7..3d42059540 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -185,7 +185,7 @@ namespace osu.Game.Overlays.Mods { Padding = new MarginPadding { - Top = (ShowTotalMultiplier ? ModsEffectDisplay.HEIGHT : 0) + PADDING, + Top = ModsEffectDisplay.HEIGHT + PADDING, Bottom = PADDING }, RelativeSizeAxes = Axes.Both, From e43c233b4879006e3cbd36eb77eaade7b23a183c Mon Sep 17 00:00:00 2001 From: Cootz Date: Sun, 28 May 2023 13:21:41 +0300 Subject: [PATCH 0059/2100] Reword `ForcedSearchTerm` xmldoc --- osu.Game/Overlays/Mods/ModSearchContainer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSearchContainer.cs b/osu.Game/Overlays/Mods/ModSearchContainer.cs index b959274391..132c02db1e 100644 --- a/osu.Game/Overlays/Mods/ModSearchContainer.cs +++ b/osu.Game/Overlays/Mods/ModSearchContainer.cs @@ -8,10 +8,11 @@ namespace osu.Game.Overlays.Mods public partial class ModSearchContainer : SearchContainer { /// - /// A string that should match the children + /// Same as except the filtering is guarantied to be performed /// /// - /// Same as except the filtering is guarantied to be performed even when can't be run. + /// This is required because can be hidden when search term applied + /// therefore cannot be reached and filter cannot automatically re-validate itself. /// public string ForcedSearchTerm { From 22c6d6c5262a97af2f1a129c2a6a6292ed76bad2 Mon Sep 17 00:00:00 2001 From: Cootz Date: Mon, 29 May 2023 14:22:40 +0300 Subject: [PATCH 0060/2100] Prevent checkbox from toggle on when column have no valid panels --- osu.Game/Overlays/Mods/ModColumn.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 71d964c618..f7d7d4db73 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -151,7 +151,10 @@ namespace osu.Game.Overlays.Mods if (toggleAllCheckbox != null && !SelectionAnimationRunning) { toggleAllCheckbox.Alpha = availableMods.Any(panel => panel.IsValid) ? 1 : 0; - toggleAllCheckbox.Current.Value = availableMods.Where(panel => panel.IsValid).All(panel => panel.Active.Value); + + //Prevent checkbox from checking when column have on valid panels + if (availableMods.Any(panel => panel.IsValid)) + toggleAllCheckbox.Current.Value = availableMods.Where(panel => panel.IsValid).All(panel => panel.Active.Value); } } From e35623df22874e4dfc3c9f9ad721bebb422aa570 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 May 2023 13:38:39 +0900 Subject: [PATCH 0061/2100] Update to use new `Filter` method and remove silly `ForcedSearchTerm` --- osu.Game/Overlays/Mods/ModSearchContainer.cs | 17 +++++++---------- osu.Game/Overlays/Mods/ModSelectColumn.cs | 2 +- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSearchContainer.cs b/osu.Game/Overlays/Mods/ModSearchContainer.cs index 132c02db1e..784322b892 100644 --- a/osu.Game/Overlays/Mods/ModSearchContainer.cs +++ b/osu.Game/Overlays/Mods/ModSearchContainer.cs @@ -7,23 +7,20 @@ namespace osu.Game.Overlays.Mods { public partial class ModSearchContainer : SearchContainer { - /// - /// Same as except the filtering is guarantied to be performed - /// - /// - /// This is required because can be hidden when search term applied - /// therefore cannot be reached and filter cannot automatically re-validate itself. - /// - public string ForcedSearchTerm + public new string SearchTerm { - get => SearchTerm; + get => base.SearchTerm; set { if (value == SearchTerm) return; SearchTerm = value; - Update(); + + // Manual filtering here is required because ModColumn can be hidden when search term applied, + // causing the whole SearchContainer to become non-present and never actually perform a subsequent + // filter. + Filter(); } } } diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index c80eb8c09c..338ebdaef4 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -45,7 +45,7 @@ namespace osu.Game.Overlays.Mods public string SearchTerm { - set => ItemsFlow.ForcedSearchTerm = value; + set => ItemsFlow.SearchTerm = value; } protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value; From ed850196d9b780b4cd30511f8b0c37750a0c30ab Mon Sep 17 00:00:00 2001 From: John Date: Tue, 30 May 2023 01:43:08 -0700 Subject: [PATCH 0062/2100] Reverted to applying the color change in OnUpdate, removed EnableComboColour flag from DrawableHitObject.cs --- osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs | 11 ++++------- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 6 ------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs index 4fda6130c0..f6fffaf736 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs @@ -31,13 +31,10 @@ namespace osu.Game.Rulesets.Osu.Mods { if (currentBeatmap.IsNull() || drawable.IsNull()) return; - drawable.ApplyCustomUpdateState += (drawableObject, state) => - { - int snapDivisor = currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(drawableObject.HitObject.StartTime); - - drawableObject.EnableComboColour = false; - drawableObject.AccentColour.Value = BindableBeatDivisor.GetColourFor(snapDivisor, colours); - }; + drawable.OnUpdate += _ => + drawable.AccentColour.Value = BindableBeatDivisor.GetColourFor( + currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(drawable.HitObject.StartTime), + colours); } } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index cbaa07bebc..07c0d1f8a1 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -74,11 +74,6 @@ namespace osu.Game.Rulesets.Objects.Drawables public override bool PropagateNonPositionalInputSubTree => HandleUserInput; - /// - /// Whether this object should be coloured using its combo position - /// - public bool EnableComboColour { get; set; } = true; - /// /// Invoked by this or a nested after a has been applied. /// @@ -533,7 +528,6 @@ namespace osu.Game.Rulesets.Objects.Drawables protected void UpdateComboColour() { if (!(HitObject is IHasComboInformation combo)) return; - if (!EnableComboColour) return; Color4 colour = combo.GetComboColour(CurrentSkin); From 6e00b21a3200ceb5f416d19ecc6cb41fd62e7b6d Mon Sep 17 00:00:00 2001 From: Cootz Date: Wed, 31 May 2023 19:16:16 +0300 Subject: [PATCH 0063/2100] Update framework version --- osu.Game/osu.Game.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 0fd2b0c2c5..db6d9b07cd 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -1,4 +1,4 @@ - + net6.0 Library @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + From 5a1c3aeb7e7ef0b735ef8f6fa071afd216f192ca Mon Sep 17 00:00:00 2001 From: Cootz Date: Wed, 31 May 2023 19:36:42 +0300 Subject: [PATCH 0064/2100] Fix `SearchTerm` set causing infinite loop --- osu.Game/Overlays/Mods/ModSearchContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSearchContainer.cs b/osu.Game/Overlays/Mods/ModSearchContainer.cs index 784322b892..8787530d5c 100644 --- a/osu.Game/Overlays/Mods/ModSearchContainer.cs +++ b/osu.Game/Overlays/Mods/ModSearchContainer.cs @@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Mods if (value == SearchTerm) return; - SearchTerm = value; + base.SearchTerm = value; // Manual filtering here is required because ModColumn can be hidden when search term applied, // causing the whole SearchContainer to become non-present and never actually perform a subsequent From f52ed41f107fe79db605fc35acab62faa9f9d9c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Jun 2023 16:28:03 +0900 Subject: [PATCH 0065/2100] Use better defaults of 1/4 and 1/6 when cycling types --- .../Screens/Edit/Compose/Components/BeatDivisorControl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 432c5ea280..e510d1aac8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -196,11 +196,11 @@ namespace osu.Game.Screens.Edit.Compose.Components switch ((BeatDivisorType)nextDivisorType) { case BeatDivisorType.Common: - beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.COMMON; + beatDivisor.SetArbitraryDivisor(4); break; case BeatDivisorType.Triplets: - beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.TRIPLETS; + beatDivisor.SetArbitraryDivisor(6); break; case BeatDivisorType.Custom: From bcde2cbc73bf31ed0d32cfe7a49e87b6afa174fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Jun 2023 16:28:47 +0900 Subject: [PATCH 0066/2100] Apply NRT to `BeatDivisorControl` --- .../Visual/Editing/TestSceneBeatDivisorControl.cs | 8 +++----- .../Screens/Edit/Compose/Components/BeatDivisorControl.cs | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs index 353acfa4ba..3ee5ea79db 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Diagnostics; using System.Linq; @@ -23,8 +21,8 @@ namespace osu.Game.Tests.Visual.Editing { public partial class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene { - private BeatDivisorControl beatDivisorControl; - private BindableBeatDivisor bindableBeatDivisor; + private BeatDivisorControl beatDivisorControl = null!; + private BindableBeatDivisor bindableBeatDivisor = null!; private SliderBar tickSliderBar => beatDivisorControl.ChildrenOfType>().Single(); private Triangle tickMarkerHead => tickSliderBar.ChildrenOfType().Single(); @@ -237,7 +235,7 @@ namespace osu.Game.Tests.Visual.Editing InputManager.Click(MouseButton.Left); }); - BeatDivisorControl.CustomDivisorPopover popover = null; + BeatDivisorControl.CustomDivisorPopover? popover = null; AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().SingleOrDefault()) != null && popover.IsLoaded); AddStep($"set divisor to {divisor}", () => { diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index e510d1aac8..213246d516 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Diagnostics; using System.Linq; @@ -372,10 +370,10 @@ namespace osu.Game.Screens.Edit.Compose.Components private partial class TickSliderBar : SliderBar { - private Marker marker; + private Marker marker = null!; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; private readonly BindableBeatDivisor beatDivisor; @@ -517,7 +515,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private partial class Marker : CompositeDrawable { [Resolved] - private OverlayColourProvider colourProvider { get; set; } + private OverlayColourProvider colourProvider { get; set; } = null!; [BackgroundDependencyLoader] private void load() From 32207d411222f1328466a6e9c40a261ebb042c16 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Jun 2023 16:40:33 +0900 Subject: [PATCH 0067/2100] Remember the last used custom divisor when cycling divisor types --- .../Compose/Components/BeatDivisorControl.cs | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 213246d516..dce9e01192 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -29,6 +29,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { public partial class BeatDivisorControl : CompositeDrawable { + private int? lastCustomDivisor; + private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); public BeatDivisorControl(BindableBeatDivisor beatDivisor) @@ -182,16 +184,30 @@ namespace osu.Game.Screens.Edit.Compose.Components }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + beatDivisor.ValidDivisors.BindValueChanged(valid => + { + if (valid.NewValue.Type == BeatDivisorType.Custom) + lastCustomDivisor = valid.NewValue.Presets.Last(); + }, true); + } + private void cycleDivisorType(int direction) { - Debug.Assert(Math.Abs(direction) == 1); - int nextDivisorType = (int)beatDivisor.ValidDivisors.Value.Type + direction; - if (nextDivisorType > (int)BeatDivisorType.Triplets) - nextDivisorType = (int)BeatDivisorType.Common; - else if (nextDivisorType < (int)BeatDivisorType.Common) - nextDivisorType = (int)BeatDivisorType.Triplets; + int totalTypes = Enum.GetValues().Length; + BeatDivisorType currentType = beatDivisor.ValidDivisors.Value.Type; - switch ((BeatDivisorType)nextDivisorType) + Debug.Assert(Math.Abs(direction) == 1); + + cycleOnce(); + + if (lastCustomDivisor == null && currentType == BeatDivisorType.Custom) + cycleOnce(); + + switch (currentType) { case BeatDivisorType.Common: beatDivisor.SetArbitraryDivisor(4); @@ -202,9 +218,12 @@ namespace osu.Game.Screens.Edit.Compose.Components break; case BeatDivisorType.Custom: - beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.Custom(beatDivisor.ValidDivisors.Value.Presets.Max()); + Debug.Assert(lastCustomDivisor != null); + beatDivisor.SetArbitraryDivisor(lastCustomDivisor.Value); break; } + + void cycleOnce() => currentType = (BeatDivisorType)(((int)currentType + totalTypes + direction) % totalTypes); } protected override bool OnKeyDown(KeyDownEvent e) @@ -302,12 +321,12 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.LoadComplete(); BeatDivisor.BindValueChanged(_ => updateState(), true); - divisorTextBox.OnCommit += (_, _) => setPresets(); + divisorTextBox.OnCommit += (_, _) => setPresetsFromTextBoxEntry(); Schedule(() => GetContainingInputManager().ChangeFocus(divisorTextBox)); } - private void setPresets() + private void setPresetsFromTextBoxEntry() { if (!int.TryParse(divisorTextBox.Text, out int divisor) || divisor < 1 || divisor > 64) { From 39489358fa5f91d56b54e92b4e8c8b939da5b659 Mon Sep 17 00:00:00 2001 From: Cootz Date: Thu, 1 Jun 2023 14:07:05 +0300 Subject: [PATCH 0068/2100] Apply appearance animation to `aboveColumnsContent` --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 3d42059540..a491da1f76 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -507,7 +507,7 @@ namespace osu.Game.Overlays.Mods base.PopIn(); - multiplierDisplay? + aboveColumnsContent? .FadeIn(fade_in_duration, Easing.OutQuint) .MoveToY(0, fade_in_duration, Easing.OutQuint); @@ -565,7 +565,7 @@ namespace osu.Game.Overlays.Mods base.PopOut(); - multiplierDisplay? + aboveColumnsContent? .FadeOut(fade_out_duration / 2, Easing.OutQuint) .MoveToY(-distance, fade_out_duration / 2, Easing.OutQuint); From 325c114c1c9ba4b7f1bfa0630c2d1ab0885d1cf1 Mon Sep 17 00:00:00 2001 From: Cootz Date: Fri, 2 Jun 2023 10:39:27 +0300 Subject: [PATCH 0069/2100] Remove redundant xmldocs --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index a491da1f76..af8a7bd810 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -64,9 +64,6 @@ namespace osu.Game.Overlays.Mods } } - /// - /// Search term applied on mod overlay - /// public string SearchTerm { get => SearchTextBox.Current.Value; @@ -79,9 +76,6 @@ namespace osu.Game.Overlays.Mods } } - /// - /// Search box applied on mod overlay - /// public ShearedSearchTextBox SearchTextBox { get; private set; } = null!; /// From d400387329e9c255e39a8229697ef47aeeb639cf Mon Sep 17 00:00:00 2001 From: Cootz Date: Fri, 2 Jun 2023 10:51:33 +0300 Subject: [PATCH 0070/2100] Replace `IConditionalFilterable` with `IFilterable` --- osu.Game/Overlays/Mods/ModPanel.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index cd94226d8f..e3bd3ad370 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -14,7 +14,7 @@ using osuTK; namespace osu.Game.Overlays.Mods { - public partial class ModPanel : ModSelectPanel, IConditionalFilterable + public partial class ModPanel : ModSelectPanel, IFilterable { public Mod Mod => modState.Mod; public override BindableBool Active => modState.Active; @@ -110,13 +110,6 @@ namespace osu.Game.Overlays.Mods } } - /// - /// This property is always because it affects search result - /// - private readonly BindableBool canBeShown = new BindableBool(true); - - IBindable IConditionalFilterable.CanBeShown => canBeShown; - private void updateFilterState() { this.FadeTo(IsValid ? 1 : 0); From 4c7cca101eab9879a7605c604be75c43516f26bf Mon Sep 17 00:00:00 2001 From: Cootz Date: Fri, 2 Jun 2023 11:33:38 +0300 Subject: [PATCH 0071/2100] Rename `IsValid` to `Visible` --- .../TestSceneMultiplayerMatchSubScreen.cs | 2 +- .../Visual/UserInterface/TestSceneModColumn.cs | 16 ++++++++-------- .../UserInterface/TestSceneModSelectOverlay.cs | 16 ++++++++-------- .../Mods/Input/ClassicModHotkeyHandler.cs | 2 +- .../Mods/Input/SequentialModHotkeyHandler.cs | 2 +- osu.Game/Overlays/Mods/ModColumn.cs | 10 +++++----- osu.Game/Overlays/Mods/ModPanel.cs | 6 +++--- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 6 +++--- osu.Game/Overlays/Mods/ModState.cs | 4 ++-- osu.Game/Overlays/Mods/SelectAllModsButton.cs | 2 +- 10 files changed, 33 insertions(+), 33 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 40acea475b..a41eff067b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -203,7 +203,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("mod select contains only double time mod", () => this.ChildrenOfType().Single().UserModsSelectOverlay .ChildrenOfType() - .SingleOrDefault(panel => panel.IsValid)?.Mod is OsuModDoubleTime); + .SingleOrDefault(panel => panel.Visible)?.Mod is OsuModDoubleTime); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index 394a38fe5d..255dbfcdd3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -106,26 +106,26 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase))); - AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => panel.IsValid) == 2); + AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => panel.Visible) == 2); clickToggle(); AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning); - AddAssert("only visible items selected", () => column.ChildrenOfType().Where(panel => panel.Active.Value).All(panel => panel.IsValid)); + AddAssert("only visible items selected", () => column.ChildrenOfType().Where(panel => panel.Active.Value).All(panel => panel.Visible)); AddStep("unset filter", () => setFilter(null)); - AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => panel.IsValid)); + AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => panel.Visible)); AddAssert("checkbox not selected", () => !column.ChildrenOfType().Single().Current.Value); AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase))); - AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => panel.IsValid) == 2); + AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => panel.Visible) == 2); AddAssert("checkbox selected", () => column.ChildrenOfType().Single().Current.Value); AddStep("filter out everything", () => setFilter(_ => false)); - AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => !panel.IsValid)); + AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => !panel.Visible)); AddUntilStep("checkbox hidden", () => !column.ChildrenOfType().Single().IsPresent); AddStep("inset filter", () => setFilter(null)); - AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => panel.IsValid)); + AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => panel.Visible)); AddUntilStep("checkbox visible", () => column.ChildrenOfType().Single().IsPresent); void clickToggle() => AddStep("click toggle", () => @@ -320,14 +320,14 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep("search by mod name", () => column.SearchTerm = searchTerm); - AddAssert("only hidden is visible", () => column.ChildrenOfType().Where(panel => panel.IsValid).All(panel => panel.Mod is ModHidden)); + AddAssert("only hidden is visible", () => column.ChildrenOfType().Where(panel => panel.Visible).All(panel => panel.Mod is ModHidden)); } void clearSearch() { AddStep("clear search", () => column.SearchTerm = string.Empty); - AddAssert("all mods are visible", () => column.ChildrenOfType().All(panel => panel.IsValid)); + AddAssert("all mods are visible", () => column.ChildrenOfType().All(panel => panel.Visible)); } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 22baea2581..c42f9af6df 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -490,15 +490,15 @@ namespace osu.Game.Tests.Visual.UserInterface createScreen(); changeRuleset(0); - AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.IsValid)); + AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.Visible)); AddStep("make double time invalid", () => modSelectOverlay.IsValidMod = m => !(m is OsuModDoubleTime)); - AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => !panel.IsValid)); - AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModNightcore).Any(panel => panel.IsValid)); + AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => !panel.Visible)); + AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModNightcore).Any(panel => panel.Visible)); AddStep("make double time valid again", () => modSelectOverlay.IsValidMod = _ => true); - AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.IsValid)); - AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(b => b.Mod is OsuModNightcore).Any(panel => panel.IsValid)); + AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.Visible)); + AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(b => b.Mod is OsuModNightcore).Any(panel => panel.Visible)); } [Test] @@ -524,7 +524,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo); waitForColumnLoad(); - AddAssert("unimplemented mod panel is filtered", () => !getPanelForMod(typeof(TestUnimplementedMod)).IsValid); + AddAssert("unimplemented mod panel is filtered", () => !getPanelForMod(typeof(TestUnimplementedMod)).Visible); } [Test] @@ -585,7 +585,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() }); AddStep("focus on search", () => modSelectOverlay.SearchTextBox.TakeFocus()); AddStep("apply search", () => modSelectOverlay.SearchTerm = "Easy"); - AddAssert("DT + HD selected and hidden", () => modSelectOverlay.ChildrenOfType().Count(panel => !panel.IsValid && panel.Active.Value) == 2); + AddAssert("DT + HD selected and hidden", () => modSelectOverlay.ChildrenOfType().Count(panel => !panel.Visible && panel.Active.Value) == 2); AddStep("press backspace", () => InputManager.Key(Key.BackSpace)); AddAssert("DT + HD still selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 2); @@ -630,7 +630,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("deselect all button enabled", () => this.ChildrenOfType().Single().Enabled.Value); AddStep("apply search", () => modSelectOverlay.SearchTerm = "Easy"); - AddAssert("DT + HD + RD are hidden and selected", () => modSelectOverlay.ChildrenOfType().Count(panel => !panel.IsValid && panel.Active.Value) == 3); + AddAssert("DT + HD + RD are hidden and selected", () => modSelectOverlay.ChildrenOfType().Count(panel => !panel.Visible && panel.Active.Value) == 3); AddAssert("deselect all button enabled", () => this.ChildrenOfType().Single().Enabled.Value); AddStep("click deselect all button", () => diff --git a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs index 343f7242f1..59a631a7b5 100644 --- a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs +++ b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs @@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Mods.Input if (!mod_type_lookup.TryGetValue(e.Key, out var typesToMatch)) return false; - var matchingMods = availableMods.Where(modState => matches(modState, typesToMatch) && modState.IsValid).ToArray(); + var matchingMods = availableMods.Where(modState => matches(modState, typesToMatch) && modState.Visible).ToArray(); if (matchingMods.Length == 0) return false; diff --git a/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs index cbfa96307d..e638063438 100644 --- a/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs +++ b/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Mods.Input if (index < 0) return false; - var modState = availableMods.Where(modState => modState.IsValid).ElementAtOrDefault(index); + var modState = availableMods.Where(modState => modState.Visible).ElementAtOrDefault(index); if (modState == null) return false; diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index f7d7d4db73..9146cd7abe 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -146,15 +146,15 @@ namespace osu.Game.Overlays.Mods private void updateState() { - Alpha = availableMods.All(mod => !mod.IsValid) ? 0 : 1; + Alpha = availableMods.All(mod => !mod.Visible) ? 0 : 1; if (toggleAllCheckbox != null && !SelectionAnimationRunning) { - toggleAllCheckbox.Alpha = availableMods.Any(panel => panel.IsValid) ? 1 : 0; + toggleAllCheckbox.Alpha = availableMods.Any(panel => panel.Visible) ? 1 : 0; //Prevent checkbox from checking when column have on valid panels - if (availableMods.Any(panel => panel.IsValid)) - toggleAllCheckbox.Current.Value = availableMods.Where(panel => panel.IsValid).All(panel => panel.Active.Value); + if (availableMods.Any(panel => panel.Visible)) + toggleAllCheckbox.Current.Value = availableMods.Where(panel => panel.Visible).All(panel => panel.Active.Value); } } @@ -199,7 +199,7 @@ namespace osu.Game.Overlays.Mods { pendingSelectionOperations.Clear(); - foreach (var button in availableMods.Where(b => !b.Active.Value && b.IsValid)) + foreach (var button in availableMods.Where(b => !b.Active.Value && b.Visible)) pendingSelectionOperations.Enqueue(() => button.Active.Value = true); } diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index e3bd3ad370..86ecdfa31d 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -73,9 +73,9 @@ namespace osu.Game.Overlays.Mods } /// - /// Determine if is valid and can be shown + /// Whether the is passing all filters and visible for user /// - public bool IsValid => modState.IsValid; + public bool Visible => modState.Visible; public bool ValidForSelection { @@ -112,7 +112,7 @@ namespace osu.Game.Overlays.Mods private void updateFilterState() { - this.FadeTo(IsValid ? 1 : 0); + this.FadeTo(Visible ? 1 : 0); } #endregion diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index af8a7bd810..1d5849e3f2 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -511,7 +511,7 @@ namespace osu.Game.Overlays.Mods { var column = columnFlow[i].Column; - bool allFiltered = column is ModColumn modColumn && modColumn.AvailableMods.All(modState => !modState.IsValid); + bool allFiltered = column is ModColumn modColumn && modColumn.AvailableMods.All(modState => !modState.Visible); double delay = allFiltered ? 0 : nonFilteredColumnCount * 30; double duration = allFiltered ? 0 : fade_in_duration; @@ -573,7 +573,7 @@ namespace osu.Game.Overlays.Mods if (column is ModColumn modColumn) { - allFiltered = modColumn.AvailableMods.All(modState => !modState.IsValid); + allFiltered = modColumn.AvailableMods.All(modState => !modState.Visible); modColumn.FlushPendingSelections(); } @@ -623,7 +623,7 @@ namespace osu.Game.Overlays.Mods return true; } - ModState? firstMod = columnFlow.Columns.OfType().FirstOrDefault(m => m.IsPresent)?.AvailableMods.FirstOrDefault(x => x.IsValid); + ModState? firstMod = columnFlow.Columns.OfType().FirstOrDefault(m => m.IsPresent)?.AvailableMods.FirstOrDefault(x => x.Visible); if (firstMod is not null) firstMod.Active.Value = !firstMod.Active.Value; diff --git a/osu.Game/Overlays/Mods/ModState.cs b/osu.Game/Overlays/Mods/ModState.cs index be770ec937..5e0d768021 100644 --- a/osu.Game/Overlays/Mods/ModState.cs +++ b/osu.Game/Overlays/Mods/ModState.cs @@ -39,9 +39,9 @@ namespace osu.Game.Overlays.Mods public BindableBool ValidForSelection { get; } = new BindableBool(true); /// - /// Determine if is valid and can be shown + /// Whether the is passing all filters and visible for user /// - public bool IsValid => MatchingFilter.Value && ValidForSelection.Value; + public bool Visible => MatchingFilter.Value && ValidForSelection.Value; /// /// Whether the mod is matching the current filter, i.e. it is available for user selection. diff --git a/osu.Game/Overlays/Mods/SelectAllModsButton.cs b/osu.Game/Overlays/Mods/SelectAllModsButton.cs index dad4f7b629..8a6180a7c8 100644 --- a/osu.Game/Overlays/Mods/SelectAllModsButton.cs +++ b/osu.Game/Overlays/Mods/SelectAllModsButton.cs @@ -44,7 +44,7 @@ namespace osu.Game.Overlays.Mods { Enabled.Value = availableMods.Value .SelectMany(pair => pair.Value) - .Any(modState => !modState.Active.Value && modState.IsValid); + .Any(modState => !modState.Active.Value && modState.Visible); } public bool OnPressed(KeyBindingPressEvent e) From 02111e38542edaa0657c9e1ea7ac48ed4b0d10f0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 1 Jun 2023 13:22:37 +0900 Subject: [PATCH 0072/2100] Implement ScoreV1 calculation for OsuRuleset --- .../Difficulty/OsuDifficultyCalculator.cs | 149 +++++++++++++++++- .../Difficulty/DifficultyAttributes.cs | 18 ++- 2 files changed, 162 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 1e83d6d820..0e06e1e28f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -11,6 +11,8 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; @@ -26,9 +28,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty public override int Version => 20220902; + private readonly IWorkingBeatmap workingBeatmap; + public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { + workingBeatmap = beatmap; } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) @@ -71,7 +76,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1 ); - double starRating = basePerformance > 0.00001 ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; + double starRating = basePerformance > 0.00001 + ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) + : 0; double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double drainRate = beatmap.Difficulty.DrainRate; @@ -90,6 +97,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { StarRating = starRating, Mods = mods, + TotalScoreV1 = new OsuScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods).TotalScore, AimDifficulty = aimRating, SpeedDifficulty = speedRating, SpeedNoteCount = speedNotes, @@ -142,4 +150,143 @@ namespace osu.Game.Rulesets.Osu.Difficulty new MultiMod(new OsuModFlashlight(), new OsuModHidden()) }; } + + public abstract class ScoreV1Processor + { + protected readonly int DifficultyPeppyStars; + protected readonly double ScoreMultiplier; + + protected readonly IBeatmap PlayableBeatmap; + + protected ScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + { + PlayableBeatmap = playableBeatmap; + + int countNormal = 0; + int countSlider = 0; + int countSpinner = 0; + + foreach (HitObject obj in baseBeatmap.HitObjects) + { + switch (obj) + { + case IHasPath: + countSlider++; + break; + + case IHasDuration: + countSpinner++; + break; + + default: + countNormal++; + break; + } + } + + int objectCount = countNormal + countSlider + countSpinner; + + DifficultyPeppyStars = (int)Math.Round( + (playableBeatmap.Difficulty.DrainRate + + playableBeatmap.Difficulty.OverallDifficulty + + playableBeatmap.Difficulty.CircleSize + + Math.Clamp(objectCount / playableBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); + + ScoreMultiplier = 1 * DifficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + } + } + + public class OsuScoreV1Processor : ScoreV1Processor + { + public int TotalScore { get; private set; } + private int combo; + + public OsuScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + : base(baseBeatmap, playableBeatmap, mods) + { + foreach (var obj in playableBeatmap.HitObjects) + increaseScore(obj); + } + + private void increaseScore(HitObject hitObject) + { + bool increaseCombo = true; + bool addScoreComboMultiplier = false; + int scoreIncrease = 0; + + switch (hitObject) + { + case SliderHeadCircle: + case SliderTailCircle: + case SliderRepeat: + scoreIncrease = 30; + break; + + case SliderTick: + scoreIncrease = 10; + break; + + case SpinnerBonusTick: + scoreIncrease = 1100; + increaseCombo = false; + break; + + case SpinnerTick: + scoreIncrease = 100; + increaseCombo = false; + break; + + case HitCircle: + scoreIncrease = 300; + addScoreComboMultiplier = true; + break; + + case Slider: + foreach (var nested in hitObject.NestedHitObjects) + increaseScore(nested); + + scoreIncrease = 300; + increaseCombo = false; + addScoreComboMultiplier = true; + break; + + case Spinner spinner: + // The spinner object applies a lenience because gameplay mechanics differ from osu-stable. + // We'll redo the calculations to match osu-stable here... + const double maximum_rotations_per_second = 477.0 / 60; + double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(PlayableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5); + double secondsDuration = spinner.Duration / 1000; + + // The total amount of half spins possible for the entire spinner. + int totalHalfSpinsPossible = (int)(secondsDuration * maximum_rotations_per_second * 2); + // The amount of half spins that are required to successfully complete the spinner (i.e. get a 300). + int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond); + // To be able to receive bonus points, the spinner must be rotated another 1.5 times. + int halfSpinsRequiredBeforeBonus = halfSpinsRequiredForCompletion + 3; + + for (int i = 0; i <= totalHalfSpinsPossible; i++) + { + if (i > halfSpinsRequiredBeforeBonus && (i - halfSpinsRequiredBeforeBonus) % 2 == 0) + increaseScore(new SpinnerBonusTick()); + else if (i > 1 && i % 2 == 0) + increaseScore(new SpinnerTick()); + } + + scoreIncrease = 300; + addScoreComboMultiplier = true; + break; + } + + if (addScoreComboMultiplier) + { + // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) + scoreIncrease += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * ScoreMultiplier)); + } + + if (increaseCombo) + combo++; + + TotalScore += scoreIncrease; + } + } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index bd45482235..d0fbd0afaf 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -27,6 +26,7 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_FLASHLIGHT = 17; protected const int ATTRIB_ID_SLIDER_FACTOR = 19; protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21; + protected const int ATTRIB_ID_TOTAL_SCORE_V1 = 23; /// /// The mods which were applied to the beatmap. @@ -36,15 +36,21 @@ namespace osu.Game.Rulesets.Difficulty /// /// The combined star rating of all skills. /// - [JsonProperty("star_rating", Order = -3)] + [JsonProperty("star_rating", Order = -4)] public double StarRating { get; set; } /// /// The maximum achievable combo. /// - [JsonProperty("max_combo", Order = -2)] + [JsonProperty("max_combo", Order = -3)] public int MaxCombo { get; set; } + /// + /// The total score achievable in ScoreV1. + /// + [JsonProperty("total_score_v1", Order = -2)] + public int TotalScoreV1 { get; set; } + /// /// Creates new . /// @@ -69,7 +75,10 @@ namespace osu.Game.Rulesets.Difficulty /// /// See: osu_difficulty_attribs table. /// - public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() => Enumerable.Empty<(int, object)>(); + public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() + { + yield return (ATTRIB_ID_TOTAL_SCORE_V1, TotalScoreV1); + } /// /// Reads osu-web database attribute mappings into this object. @@ -78,6 +87,7 @@ namespace osu.Game.Rulesets.Difficulty /// The where more information about the beatmap may be extracted from (such as AR/CS/OD/etc). public virtual void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { + TotalScoreV1 = (int)values[ATTRIB_ID_TOTAL_SCORE_V1]; } } } From 9d4ba5d64ab354b8163efc73a6287ec610a32fa9 Mon Sep 17 00:00:00 2001 From: Cootz Date: Fri, 2 Jun 2023 12:00:03 +0300 Subject: [PATCH 0073/2100] Remove unnecessary null checks --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 1d5849e3f2..ad5d571535 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -501,7 +501,7 @@ namespace osu.Game.Overlays.Mods base.PopIn(); - aboveColumnsContent? + aboveColumnsContent .FadeIn(fade_in_duration, Easing.OutQuint) .MoveToY(0, fade_in_duration, Easing.OutQuint); @@ -559,7 +559,7 @@ namespace osu.Game.Overlays.Mods base.PopOut(); - aboveColumnsContent? + aboveColumnsContent .FadeOut(fade_out_duration / 2, Easing.OutQuint) .MoveToY(-distance, fade_out_duration / 2, Easing.OutQuint); From e402c6d2b4275b87468d1280826ae1c96d338c46 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 2 Jun 2023 21:53:25 +0900 Subject: [PATCH 0074/2100] Write max combo attribute from base class --- osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs | 2 -- osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs | 2 -- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs | 2 -- osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs | 2 -- osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs | 2 ++ 5 files changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs index 2d01153f98..5c64643fd4 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs @@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty // Todo: osu!catch should not output star rating in the 'aim' attribute. yield return (ATTRIB_ID_AIM, StarRating); yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate); - yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -36,7 +35,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty StarRating = values[ATTRIB_ID_AIM]; ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; - MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; } } } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs index d259c2af8e..db60e757e1 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs @@ -24,7 +24,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty foreach (var v in base.ToDatabaseAttributes()) yield return v; - yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); yield return (ATTRIB_ID_DIFFICULTY, StarRating); yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); } @@ -33,7 +32,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty { base.FromDatabaseAttributes(values, onlineInfo); - MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; StarRating = values[ATTRIB_ID_DIFFICULTY]; GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 03540abddb..24d5635104 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -93,7 +93,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_SPEED, SpeedDifficulty); yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty); yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate); - yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); yield return (ATTRIB_ID_DIFFICULTY, StarRating); if (ShouldSerializeFlashlightRating()) @@ -111,7 +110,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty SpeedDifficulty = values[ATTRIB_ID_SPEED]; OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY]; ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; - MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; StarRating = values[ATTRIB_ID_DIFFICULTY]; FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT); SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR]; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index 72452e27b3..1664c941f8 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -48,7 +48,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty foreach (var v in base.ToDatabaseAttributes()) yield return v; - yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); yield return (ATTRIB_ID_DIFFICULTY, StarRating); yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); } @@ -57,7 +56,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { base.FromDatabaseAttributes(values, onlineInfo); - MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; StarRating = values[ATTRIB_ID_DIFFICULTY]; GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index d0fbd0afaf..ee02376939 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -77,6 +77,7 @@ namespace osu.Game.Rulesets.Difficulty /// public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { + yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); yield return (ATTRIB_ID_TOTAL_SCORE_V1, TotalScoreV1); } @@ -87,6 +88,7 @@ namespace osu.Game.Rulesets.Difficulty /// The where more information about the beatmap may be extracted from (such as AR/CS/OD/etc). public virtual void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { + MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; TotalScoreV1 = (int)values[ATTRIB_ID_TOTAL_SCORE_V1]; } } From 69b640a185a1850159064a2701cba7726c27da44 Mon Sep 17 00:00:00 2001 From: Cootz Date: Sat, 3 Jun 2023 10:47:48 +0300 Subject: [PATCH 0075/2100] Remove hotkeys handling from `DeselectAllModsButton` --- osu.Game/Overlays/Mods/DeselectAllModsButton.cs | 5 +---- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/osu.Game/Overlays/Mods/DeselectAllModsButton.cs b/osu.Game/Overlays/Mods/DeselectAllModsButton.cs index bdb37e3ead..3e5a3b12d1 100644 --- a/osu.Game/Overlays/Mods/DeselectAllModsButton.cs +++ b/osu.Game/Overlays/Mods/DeselectAllModsButton.cs @@ -18,7 +18,6 @@ namespace osu.Game.Overlays.Mods public partial class DeselectAllModsButton : ShearedButton, IKeyBindingHandler { private readonly Bindable> selectedMods = new Bindable>(); - private readonly ShearedSearchTextBox searchTextBox; public DeselectAllModsButton(ModSelectOverlay modSelectOverlay) : base(ModSelectOverlay.BUTTON_WIDTH) @@ -26,8 +25,6 @@ namespace osu.Game.Overlays.Mods Text = CommonStrings.DeselectAll; Action = modSelectOverlay.DeselectAll; - searchTextBox = modSelectOverlay.SearchTextBox; - selectedMods.BindTo(modSelectOverlay.SelectedMods); } @@ -45,7 +42,7 @@ namespace osu.Game.Overlays.Mods public bool OnPressed(KeyBindingPressEvent e) { - if (e.Repeat || e.Action != GlobalAction.DeselectAllMods || searchTextBox.HasFocus) + if (e.Repeat || e.Action != GlobalAction.DeselectAllMods) return false; TriggerClick(); diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index ad5d571535..e3977f6ecd 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -66,17 +66,17 @@ namespace osu.Game.Overlays.Mods public string SearchTerm { - get => SearchTextBox.Current.Value; + get => searchTextBox.Current.Value; set { - if (SearchTextBox.Current.Value == value) + if (searchTextBox.Current.Value == value) return; - SearchTextBox.Current.Value = value; + searchTextBox.Current.Value = value; } } - public ShearedSearchTextBox SearchTextBox { get; private set; } = null!; + private ShearedSearchTextBox searchTextBox = null!; /// /// Whether the total score multiplier calculated from the current selected set of mods should be shown. @@ -166,7 +166,7 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.X, Height = ModsEffectDisplay.HEIGHT, Padding = new MarginPadding { Horizontal = 100 }, - Child = SearchTextBox = new ShearedSearchTextBox + Child = searchTextBox = new ShearedSearchTextBox { HoldFocus = false, Width = 300 @@ -249,7 +249,7 @@ namespace osu.Game.Overlays.Mods base.Hide(); //We want to clear search for next user interaction with mod overlay - SearchTextBox.Current.Value = string.Empty; + searchTextBox.Current.Value = string.Empty; } private ModSettingChangeTracker? modSettingChangeTracker; @@ -289,7 +289,7 @@ namespace osu.Game.Overlays.Mods customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); - SearchTextBox.Current.BindValueChanged(query => + searchTextBox.Current.BindValueChanged(query => { foreach (var column in columnFlow.Columns) column.SearchTerm = query.NewValue; @@ -789,7 +789,7 @@ namespace osu.Game.Overlays.Mods if (!Active.Value) RequestScroll?.Invoke(this); - //Kill focus on SearchTextBox + //Kill focus on searchTextBox Scheduler.Add(() => GetContainingInputManager().ChangeFocus(null)); return true; From 32b9e6ec8f0a5a395d2295e0510fa3c40a058e67 Mon Sep 17 00:00:00 2001 From: Cootz Date: Sun, 4 Jun 2023 17:02:46 +0300 Subject: [PATCH 0076/2100] Make search bar active by default --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index e3977f6ecd..9d07ee1c93 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -66,17 +66,17 @@ namespace osu.Game.Overlays.Mods public string SearchTerm { - get => searchTextBox.Current.Value; + get => SearchTextBox.Current.Value; set { - if (searchTextBox.Current.Value == value) + if (SearchTextBox.Current.Value == value) return; - searchTextBox.Current.Value = value; + SearchTextBox.Current.Value = value; } } - private ShearedSearchTextBox searchTextBox = null!; + public ShearedSearchTextBox SearchTextBox { get; private set; } = null!; /// /// Whether the total score multiplier calculated from the current selected set of mods should be shown. @@ -166,7 +166,7 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.X, Height = ModsEffectDisplay.HEIGHT, Padding = new MarginPadding { Horizontal = 100 }, - Child = searchTextBox = new ShearedSearchTextBox + Child = SearchTextBox = new ShearedSearchTextBox { HoldFocus = false, Width = 300 @@ -249,7 +249,7 @@ namespace osu.Game.Overlays.Mods base.Hide(); //We want to clear search for next user interaction with mod overlay - searchTextBox.Current.Value = string.Empty; + SearchTextBox.Current.Value = string.Empty; } private ModSettingChangeTracker? modSettingChangeTracker; @@ -289,12 +289,14 @@ namespace osu.Game.Overlays.Mods customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); - searchTextBox.Current.BindValueChanged(query => + SearchTextBox.Current.BindValueChanged(query => { foreach (var column in columnFlow.Columns) column.SearchTerm = query.NewValue; }, true); + SearchTextBox.TakeFocus(); + // Start scrolled slightly to the right to give the user a sense that // there is more horizontal content available. ScheduleAfterChildren(() => @@ -789,7 +791,7 @@ namespace osu.Game.Overlays.Mods if (!Active.Value) RequestScroll?.Invoke(this); - //Kill focus on searchTextBox + //Kill focus on SearchTextBox Scheduler.Add(() => GetContainingInputManager().ChangeFocus(null)); return true; From fd554033db74781f4a40a25527c91d34d7c79cc3 Mon Sep 17 00:00:00 2001 From: Cootz Date: Sun, 4 Jun 2023 17:11:04 +0300 Subject: [PATCH 0077/2100] Update tests --- .../Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs | 2 ++ .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index 45f671618e..60bd88cc2b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -62,6 +62,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { createFreeModSelect(); + AddStep("kill search bar focus", () => freeModSelectOverlay.SearchTextBox.KillFocus()); + AddStep("press ctrl+a", () => InputManager.Keys(PlatformAction.SelectAll)); AddUntilStep("all mods selected", assertAllAvailableModsSelected); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index c42f9af6df..26369edeb6 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -23,6 +23,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Mods; using osuTK; using osuTK.Input; @@ -569,6 +570,8 @@ namespace osu.Game.Tests.Visual.UserInterface createScreen(); changeRuleset(0); + AddStep("kill search bar focus", () => modSelectOverlay.SearchTextBox.KillFocus()); + AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() }); AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 2); From a46f5b90d4c4db173675e080f4aef42f508b9e48 Mon Sep 17 00:00:00 2001 From: Cootz Date: Sun, 4 Jun 2023 18:11:44 +0300 Subject: [PATCH 0078/2100] Move focus handling into `PopIn` --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 9d07ee1c93..7380c53073 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -295,8 +295,6 @@ namespace osu.Game.Overlays.Mods column.SearchTerm = query.NewValue; }, true); - SearchTextBox.TakeFocus(); - // Start scrolled slightly to the right to give the user a sense that // there is more horizontal content available. ScheduleAfterChildren(() => @@ -503,6 +501,8 @@ namespace osu.Game.Overlays.Mods base.PopIn(); + SearchTextBox.TakeFocus(); + aboveColumnsContent .FadeIn(fade_in_duration, Easing.OutQuint) .MoveToY(0, fade_in_duration, Easing.OutQuint); From 3ebc8014847ed22b189ab1843ad8ae72bd31e8a3 Mon Sep 17 00:00:00 2001 From: Cootz Date: Mon, 5 Jun 2023 13:49:07 +0300 Subject: [PATCH 0079/2100] Move (de)select all mods hotkeys handling to `ModSelectOverlay` --- .../TestSceneModSelectOverlay.cs | 1 - .../Overlays/Mods/DeselectAllModsButton.cs | 18 +-------- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 37 ++++++++++++++++++- osu.Game/Overlays/Mods/SelectAllModsButton.cs | 18 +-------- .../OnlinePlay/FreeModSelectOverlay.cs | 14 ++++--- 5 files changed, 45 insertions(+), 43 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 26369edeb6..37b6a6d44f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -23,7 +23,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko.Mods; -using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Mods; using osuTK; using osuTK.Input; diff --git a/osu.Game/Overlays/Mods/DeselectAllModsButton.cs b/osu.Game/Overlays/Mods/DeselectAllModsButton.cs index 3e5a3b12d1..817b6beac3 100644 --- a/osu.Game/Overlays/Mods/DeselectAllModsButton.cs +++ b/osu.Game/Overlays/Mods/DeselectAllModsButton.cs @@ -6,16 +6,13 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; -using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; namespace osu.Game.Overlays.Mods { - public partial class DeselectAllModsButton : ShearedButton, IKeyBindingHandler + public partial class DeselectAllModsButton : ShearedButton { private readonly Bindable> selectedMods = new Bindable>(); @@ -39,18 +36,5 @@ namespace osu.Game.Overlays.Mods { Enabled.Value = selectedMods.Value.Any(); } - - public bool OnPressed(KeyBindingPressEvent e) - { - if (e.Repeat || e.Action != GlobalAction.DeselectAllMods) - return false; - - TriggerClick(); - return true; - } - - public void OnReleased(KeyBindingReleaseEvent e) - { - } } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 7380c53073..51e1c33124 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -12,6 +12,8 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Audio; @@ -28,7 +30,7 @@ using osuTK; namespace osu.Game.Overlays.Mods { - public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler + public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler, IKeyBindingHandler { public const int BUTTON_WIDTH = 200; @@ -108,7 +110,7 @@ namespace osu.Game.Overlays.Mods }; } - yield return new DeselectAllModsButton(this); + yield return deselectAllModsButton = new DeselectAllModsButton(this); } private readonly Bindable>> globalAvailableMods = new Bindable>>(); @@ -121,12 +123,14 @@ namespace osu.Game.Overlays.Mods private ColumnScrollContainer columnScroll = null!; private ColumnFlowContainer columnFlow = null!; private FillFlowContainer footerButtonFlow = null!; + private DeselectAllModsButton deselectAllModsButton = null!; private Container aboveColumnsContent = null!; private DifficultyMultiplierDisplay? multiplierDisplay; protected ShearedButton BackButton { get; private set; } = null!; protected ShearedToggleButton? CustomisationButton { get; private set; } + protected SelectAllModsButton? SelectAllModsButton { get; set; } private Sample? columnAppearSample; @@ -616,6 +620,18 @@ namespace osu.Game.Overlays.Mods hideOverlay(true); return true; + //This is handled locally here to prevent search box from coupling in DeselectAllModsButton + case GlobalAction.DeselectAllMods: + { + if (!SearchTextBox.HasFocus) + { + deselectAllModsButton.TriggerClick(); + return true; + } + + break; + } + case GlobalAction.Select: { // Pressing select should select first filtered mod or completely hide the overlay in one shot if search term is empty. @@ -651,6 +667,23 @@ namespace osu.Game.Overlays.Mods } } + /// + /// + /// This is handled locally here to allow handle first + /// > + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Repeat || e.Action != PlatformAction.SelectAll || SelectAllModsButton is null) + return false; + + SelectAllModsButton.TriggerClick(); + return true; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + #endregion #region Sample playback control diff --git a/osu.Game/Overlays/Mods/SelectAllModsButton.cs b/osu.Game/Overlays/Mods/SelectAllModsButton.cs index 8a6180a7c8..83c46cfc1f 100644 --- a/osu.Game/Overlays/Mods/SelectAllModsButton.cs +++ b/osu.Game/Overlays/Mods/SelectAllModsButton.cs @@ -6,9 +6,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; -using osu.Framework.Input; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; @@ -16,7 +13,7 @@ using osu.Game.Screens.OnlinePlay; namespace osu.Game.Overlays.Mods { - public partial class SelectAllModsButton : ShearedButton, IKeyBindingHandler + public partial class SelectAllModsButton : ShearedButton { private readonly Bindable> selectedMods = new Bindable>(); private readonly Bindable>> availableMods = new Bindable>>(); @@ -46,18 +43,5 @@ namespace osu.Game.Overlays.Mods .SelectMany(pair => pair.Value) .Any(modState => !modState.Active.Value && modState.Visible); } - - public bool OnPressed(KeyBindingPressEvent e) - { - if (e.Repeat || e.Action != PlatformAction.SelectAll) - return false; - - TriggerClick(); - return true; - } - - public void OnReleased(KeyBindingReleaseEvent e) - { - } } } diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 6313d907a5..d5e57b9ec9 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -34,11 +34,13 @@ namespace osu.Game.Screens.OnlinePlay protected override ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, true); - protected override IEnumerable CreateFooterButtons() => base.CreateFooterButtons().Prepend( - new SelectAllModsButton(this) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - }); + protected override IEnumerable CreateFooterButtons() + => base.CreateFooterButtons() + .Prepend( + SelectAllModsButton = new SelectAllModsButton(this) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }); } } From 71e6f80c40e36a3b3f14f8d4ccdb38e707cf6b17 Mon Sep 17 00:00:00 2001 From: Cootz Date: Mon, 5 Jun 2023 15:54:19 +0300 Subject: [PATCH 0080/2100] Add hotkey for switching search bar focus --- .../TestSceneModSelectOverlay.cs | 22 +++++++++++++++---- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 14 ++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 37b6a6d44f..ffc0a0a0ad 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -546,16 +546,16 @@ namespace osu.Game.Tests.Visual.UserInterface } [Test] - public void TestSearchFocusChange() + public void TestSearchFocusChangeViaClick() { createScreen(); - AddStep("click on search", navigateAndClick); - AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus); - AddStep("click on mod column", navigateAndClick); AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus); + AddStep("click on search", navigateAndClick); + AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus); + void navigateAndClick() where T : Drawable { InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().FirstOrDefault()); @@ -563,6 +563,20 @@ namespace osu.Game.Tests.Visual.UserInterface } } + [Test] + public void TestSearchFocusChangeViaKey() + { + createScreen(); + + const Key focus_switch_key = Key.Tab; + + AddStep("press tab", () => InputManager.Key(focus_switch_key)); + AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus); + + AddStep("press tab", () => InputManager.Key(focus_switch_key)); + AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus); + } + [Test] public void TestDeselectAllViaKey() { diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 51e1c33124..3795ed729d 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -27,6 +27,7 @@ using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Utils; using osuTK; +using osuTK.Input; namespace osu.Game.Overlays.Mods { @@ -684,6 +685,19 @@ namespace osu.Game.Overlays.Mods { } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Repeat || e.Key != Key.Tab) + return false; + + if (SearchTextBox.HasFocus) + SearchTextBox.KillFocus(); + else + SearchTextBox.TakeFocus(); + + return true; + } + #endregion #region Sample playback control From 4c397085c5203aae8c70f58a63d5a48787cdbc2f Mon Sep 17 00:00:00 2001 From: tsrk Date: Tue, 6 Jun 2023 00:04:29 +0200 Subject: [PATCH 0081/2100] refactor: improve attachement flow This removes the hard reliance on individual classes and makes its usage easiser to ingreate anywhere else. --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 52 +++++++++++++++++-- .../ClicksPerSecondCalculator.cs | 2 +- .../Screens/Play/HUD/KeyCounterDisplay.cs | 16 +++++- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index a24e22f22b..8065087341 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -158,9 +158,41 @@ namespace osu.Game.Rulesets.UI #endregion + #region Component attachement + + public void Attach(IAttachableSkinComponent skinComponent) + { + switch (skinComponent) + { + case KeyCounterDisplay keyCounterDisplay: + attachKeyCounter(keyCounterDisplay); + break; + + case ClicksPerSecondCalculator clicksPerSecondCalculator: + attachClicksPerSecond(clicksPerSecondCalculator); + break; + } + } + + public void Detach(IAttachableSkinComponent skinComponent) + { + switch (skinComponent) + { + case KeyCounterDisplay keyCounterDisplay: + detachKeyCounter(keyCounterDisplay); + break; + + case ClicksPerSecondCalculator clicksPerSecondCalculator: + detachClicksPerSecond(clicksPerSecondCalculator); + break; + } + } + + #endregion + #region Key Counter Attachment - public void Attach(KeyCounterDisplay keyCounter) + private void attachKeyCounter(KeyCounterDisplay keyCounter) { var receptor = new ActionReceptor(keyCounter); @@ -174,6 +206,10 @@ namespace osu.Game.Rulesets.UI .Select(action => new KeyCounterActionTrigger(action))); } + private void detachKeyCounter(KeyCounterDisplay keyCounter) + { + } + private partial class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler { public ActionReceptor(KeyCounterDisplay target) @@ -197,13 +233,17 @@ namespace osu.Game.Rulesets.UI #region Keys per second Counter Attachment - public void Attach(ClicksPerSecondCalculator calculator) + private void attachClicksPerSecond(ClicksPerSecondCalculator calculator) { var listener = new ActionListener(calculator); KeyBindingContainer.Add(listener); } + private void detachClicksPerSecond(ClicksPerSecondCalculator calculator) + { + } + private partial class ActionListener : Component, IKeyBindingHandler { private readonly ClicksPerSecondCalculator calculator; @@ -266,8 +306,12 @@ namespace osu.Game.Rulesets.UI /// public interface ICanAttachHUDPieces { - void Attach(KeyCounterDisplay keyCounter); - void Attach(ClicksPerSecondCalculator calculator); + void Attach(IAttachableSkinComponent component); + void Detach(IAttachableSkinComponent component); + } + + public interface IAttachableSkinComponent + { } public class RulesetInputManagerInputState : InputState diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs index ba0c47dc8b..3e55e11f1c 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Play.HUD.ClicksPerSecond { - public partial class ClicksPerSecondCalculator : Component + public partial class ClicksPerSecondCalculator : Component, IAttachableSkinComponent { private readonly List timestamps = new List(); diff --git a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs index 05427d3a32..b5c697ef13 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Configuration; +using osu.Game.Rulesets.UI; using osuTK; namespace osu.Game.Screens.Play.HUD @@ -18,7 +19,7 @@ namespace osu.Game.Screens.Play.HUD /// /// A flowing display of all gameplay keys. Individual keys can be added using implementations. /// - public abstract partial class KeyCounterDisplay : CompositeDrawable + public abstract partial class KeyCounterDisplay : CompositeDrawable, IAttachableSkinComponent { /// /// Whether the key counter should be visible regardless of the configuration value. @@ -44,6 +45,11 @@ namespace osu.Game.Screens.Play.HUD private Receptor? receptor; + /// + /// Sets a that will populate keybinding events to this . + /// + /// The receptor to set + /// When a is already active on this public void SetReceptor(Receptor receptor) { if (this.receptor != null) @@ -52,6 +58,14 @@ namespace osu.Game.Screens.Play.HUD this.receptor = receptor; } + /// + /// Clears any active + /// + public void ClearReceptor() + { + receptor = null; + } + /// /// Add a to this display. /// From 6fc6729677fb071f35d6ed203283a12f3c6cd600 Mon Sep 17 00:00:00 2001 From: tsrk Date: Tue, 6 Jun 2023 00:13:29 +0200 Subject: [PATCH 0082/2100] feat: integrate attachment flow in `SkinComponentsContainer` --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 10 +++----- osu.Game/Screens/Play/HUDOverlay.cs | 1 + osu.Game/Skinning/SkinComponentsContainer.cs | 26 ++++++++++++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 4f22c0c617..d899b6bad1 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -30,8 +30,6 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD; -using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osuTK; namespace osu.Game.Rulesets.UI @@ -329,11 +327,11 @@ namespace osu.Game.Rulesets.UI /// The representing . public abstract DrawableHitObject CreateDrawableRepresentation(TObject h); - public void Attach(KeyCounterDisplay keyCounter) => - (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(keyCounter); + public void Attach(IAttachableSkinComponent skinComponent) => + (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(skinComponent); - public void Attach(ClicksPerSecondCalculator calculator) => - (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(calculator); + public void Detach(IAttachableSkinComponent skinComponent) => + (KeyBindingInputManager as ICanAttachHUDPieces)?.Detach(skinComponent); /// /// Creates a key conversion input manager. An exception will be thrown if a valid is not returned. diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 9f050a07bd..ae0e67867b 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -321,6 +321,7 @@ namespace osu.Game.Screens.Play { attachTarget.Attach(KeyCounter); attachTarget.Attach(clicksPerSecondCalculator); + mainComponents.SetAttachTarget(attachTarget); } replayLoaded.BindTo(drawableRuleset.HasReplayLoaded); diff --git a/osu.Game/Skinning/SkinComponentsContainer.cs b/osu.Game/Skinning/SkinComponentsContainer.cs index adf0a288b4..19c16d2177 100644 --- a/osu.Game/Skinning/SkinComponentsContainer.cs +++ b/osu.Game/Skinning/SkinComponentsContainer.cs @@ -6,8 +6,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.UI; namespace osu.Game.Skinning { @@ -39,6 +41,8 @@ namespace osu.Game.Skinning private CancellationTokenSource? cancellationSource; + private ICanAttachHUDPieces? attachTarget; + public SkinComponentsContainer(SkinComponentsContainerLookup lookup) { Lookup = lookup; @@ -62,6 +66,10 @@ namespace osu.Game.Skinning public void Reload(Container? componentsContainer) { + components + .OfType() + .ForEach(c => attachTarget?.Detach(c)); + ClearInternal(); components.Clear(); ComponentsLoaded = false; @@ -77,6 +85,7 @@ namespace osu.Game.Skinning LoadComponentAsync(content, wrapper => { AddInternal(wrapper); + wrapper.Children.OfType().ForEach(c => attachTarget?.Attach(c)); components.AddRange(wrapper.Children.OfType()); ComponentsLoaded = true; }, (cancellationSource = new CancellationTokenSource()).Token); @@ -93,6 +102,9 @@ namespace osu.Game.Skinning if (!(component is Drawable drawable)) throw new ArgumentException($"Provided argument must be of type {nameof(Drawable)}.", nameof(component)); + if (component is IAttachableSkinComponent attachableSkinComponent) + attachTarget?.Attach(attachableSkinComponent); + content.Add(drawable); components.Add(component); } @@ -108,10 +120,24 @@ namespace osu.Game.Skinning if (!(component is Drawable drawable)) throw new ArgumentException($"Provided argument must be of type {nameof(Drawable)}.", nameof(component)); + if (component is IAttachableSkinComponent attachableSkinComponent) + attachTarget?.Detach(attachableSkinComponent); + content.Remove(drawable, disposeImmediately); components.Remove(component); } + public void SetAttachTarget(ICanAttachHUDPieces target) + { + attachTarget = target; + + foreach (var child in InternalChildren) + { + if (child is IAttachableSkinComponent attachable) + attachTarget.Attach(attachable); + } + } + protected override void SkinChanged(ISkinSource skin) { base.SkinChanged(skin); From 77c745cc94d26d5998ba121c4b5fbe137a1af09c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 6 Jun 2023 17:25:28 +0900 Subject: [PATCH 0083/2100] "TotalScoreV1" -> "LegacyTotalScore" --- .../Difficulty/OsuDifficultyCalculator.cs | 2 +- osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 0e06e1e28f..1011066892 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { StarRating = starRating, Mods = mods, - TotalScoreV1 = new OsuScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods).TotalScore, + LegacyTotalScore = new OsuScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods).TotalScore, AimDifficulty = aimRating, SpeedDifficulty = speedRating, SpeedNoteCount = speedNotes, diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index ee02376939..8e30050a7f 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_FLASHLIGHT = 17; protected const int ATTRIB_ID_SLIDER_FACTOR = 19; protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21; - protected const int ATTRIB_ID_TOTAL_SCORE_V1 = 23; + protected const int ATTRIB_ID_LEGACY_TOTAL_SCORE = 23; /// /// The mods which were applied to the beatmap. @@ -46,10 +46,10 @@ namespace osu.Game.Rulesets.Difficulty public int MaxCombo { get; set; } /// - /// The total score achievable in ScoreV1. + /// The maximum achievable legacy total score. /// - [JsonProperty("total_score_v1", Order = -2)] - public int TotalScoreV1 { get; set; } + [JsonProperty("legacy_total_score", Order = -2)] + public int LegacyTotalScore { get; set; } /// /// Creates new . @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Difficulty public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); - yield return (ATTRIB_ID_TOTAL_SCORE_V1, TotalScoreV1); + yield return (ATTRIB_ID_LEGACY_TOTAL_SCORE, LegacyTotalScore); } /// @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Difficulty public virtual void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; - TotalScoreV1 = (int)values[ATTRIB_ID_TOTAL_SCORE_V1]; + LegacyTotalScore = (int)values[ATTRIB_ID_LEGACY_TOTAL_SCORE]; } } } From ba7069df34b65ee70ecf9962d240c3deb05dc68a Mon Sep 17 00:00:00 2001 From: Cootz Date: Tue, 6 Jun 2023 16:12:31 +0300 Subject: [PATCH 0084/2100] =?UTF-8?q?Fix=20`SelectAllModsButton`=20state?= =?UTF-8?q?=20doesn=E2=80=99t=20update=20when=20search=20term=20changed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TestSceneFreeModSelectOverlay.cs | 24 +++++++++++++++++++ osu.Game/Overlays/Mods/SelectAllModsButton.cs | 3 +++ 2 files changed, 27 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index 60bd88cc2b..66ba908879 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -8,6 +8,7 @@ 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.Input; using osu.Framework.Testing; @@ -57,6 +58,29 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("customisation area not expanded", () => this.ChildrenOfType().Single().Height == 0); } + [Test] + public void TestSelectAllButtonUpdatesStateWhenSearchTermChanged() + { + createFreeModSelect(); + + AddStep("apply search term", () => freeModSelectOverlay.SearchTerm = "ea"); + + AddAssert("select all button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + + AddStep("click select all button", navigateAndClick); + AddAssert("select all button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + + AddStep("change search term", () => freeModSelectOverlay.SearchTerm = "e"); + + AddAssert("select all button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + + void navigateAndClick() where T : Drawable + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + } + } + [Test] public void TestSelectDeselectAllViaKeyboard() { diff --git a/osu.Game/Overlays/Mods/SelectAllModsButton.cs b/osu.Game/Overlays/Mods/SelectAllModsButton.cs index 83c46cfc1f..dd14514a3b 100644 --- a/osu.Game/Overlays/Mods/SelectAllModsButton.cs +++ b/osu.Game/Overlays/Mods/SelectAllModsButton.cs @@ -17,6 +17,7 @@ namespace osu.Game.Overlays.Mods { private readonly Bindable> selectedMods = new Bindable>(); private readonly Bindable>> availableMods = new Bindable>>(); + private readonly Bindable searchTerm = new Bindable(); public SelectAllModsButton(FreeModSelectOverlay modSelectOverlay) : base(ModSelectOverlay.BUTTON_WIDTH) @@ -26,6 +27,7 @@ namespace osu.Game.Overlays.Mods selectedMods.BindTo(modSelectOverlay.SelectedMods); availableMods.BindTo(modSelectOverlay.AvailableMods); + searchTerm.BindTo(modSelectOverlay.SearchTextBox.Current); } protected override void LoadComplete() @@ -34,6 +36,7 @@ namespace osu.Game.Overlays.Mods selectedMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState)); availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState)); + searchTerm.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState)); updateEnabledState(); } From d10c63ed2de6e0dbacee501d17ff9b8e41cb0b0a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 8 Jun 2023 16:29:34 +0900 Subject: [PATCH 0085/2100] Fix difficulty calculation when mods are involved --- .../Difficulty/OsuDifficultyCalculator.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 1011066892..5292707ac1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -187,12 +187,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty int objectCount = countNormal + countSlider + countSpinner; DifficultyPeppyStars = (int)Math.Round( - (playableBeatmap.Difficulty.DrainRate - + playableBeatmap.Difficulty.OverallDifficulty - + playableBeatmap.Difficulty.CircleSize - + Math.Clamp(objectCount / playableBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); + (baseBeatmap.Difficulty.DrainRate + + baseBeatmap.Difficulty.OverallDifficulty + + baseBeatmap.Difficulty.CircleSize + + Math.Clamp(objectCount / baseBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); - ScoreMultiplier = 1 * DifficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + ScoreMultiplier = DifficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); } } From 3978d4babb86917150c7bfb5a19ce3c72830b885 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Jun 2023 16:30:14 +0900 Subject: [PATCH 0086/2100] Crop and disable mipmaps on beatmap panel backgrounds This is an effort to improve general performance at song select. At least on the metal renderer, I can notice very high draw frame overheads related to texture uploads. By reducing the size of the texture uploads to roughly match what is actually being displayed on screen (using a relatively inexpensive crop operation), we can bastly reduce stuttering both during initial load and carousel scroll. You might ask if it's safe to disable mipmapping, but I've tested with lower resolutions and bilinear filtering seems to handle just fine. Bilinear without mipmaps only falls apart when you scale below 50% and we're not going too far past that at minimum game scale, if at all. --- ...eatmapPanelBackgroundTextureLoaderStore.cs | 88 +++++++++++++++++++ osu.Game/Beatmaps/IBeatmapResourceProvider.cs | 5 ++ osu.Game/Beatmaps/IWorkingBeatmap.cs | 5 ++ osu.Game/Beatmaps/WorkingBeatmap.cs | 1 + osu.Game/Beatmaps/WorkingBeatmapCache.cs | 11 ++- .../Select/Carousel/SetPanelBackground.cs | 24 ++++- 6 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs diff --git a/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs new file mode 100644 index 0000000000..0ee516a912 --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs @@ -0,0 +1,88 @@ +// 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.Threading; +using System.Threading.Tasks; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +namespace osu.Game.Beatmaps +{ + // Implementation of this class is based off of `MaxDimensionLimitedTextureLoaderStore`. + // If issues are found it's worth checking to make sure similar issues exist there. + public class BeatmapPanelBackgroundTextureLoaderStore : IResourceStore + { + // These numbers are taken from the draw visualiser size requirements for song select panel textures at extreme aspect ratios. + private const int height = 130; + private const int max_width = 1280; + + private readonly IResourceStore? textureStore; + + public BeatmapPanelBackgroundTextureLoaderStore(IResourceStore? textureStore) + { + this.textureStore = textureStore; + } + + public void Dispose() + { + textureStore?.Dispose(); + } + + public TextureUpload Get(string name) + { + var textureUpload = textureStore?.Get(name); + + // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp. + if (textureUpload == null) + return null!; + + return limitTextureUploadSize(textureUpload); + } + + public async Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) + { + // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp. + if (textureStore == null) + return null!; + + var textureUpload = await textureStore.GetAsync(name, cancellationToken).ConfigureAwait(false); + + if (textureUpload == null) + return null!; + + return await Task.Run(() => limitTextureUploadSize(textureUpload), cancellationToken).ConfigureAwait(false); + } + + private TextureUpload limitTextureUploadSize(TextureUpload textureUpload) + { + var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height); + + // The original texture upload will no longer be returned or used. + textureUpload.Dispose(); + + Size size = image.Size(); + int usableWidth = Math.Min(max_width, size.Width); + + // Crop the centre region of the background for now. + Rectangle cropRectangle = new Rectangle( + (size.Width - usableWidth) / 2, + size.Height / 2 - height / 2, + usableWidth, + height + ); + + image.Mutate(i => i.Crop(cropRectangle)); + + return new TextureUpload(image); + } + + public Stream? GetStream(string name) => textureStore?.GetStream(name); + + public IEnumerable GetAvailableResources() => textureStore?.GetAvailableResources() ?? Array.Empty(); + } +} diff --git a/osu.Game/Beatmaps/IBeatmapResourceProvider.cs b/osu.Game/Beatmaps/IBeatmapResourceProvider.cs index 22ff7ce8c8..d033608c95 100644 --- a/osu.Game/Beatmaps/IBeatmapResourceProvider.cs +++ b/osu.Game/Beatmaps/IBeatmapResourceProvider.cs @@ -16,6 +16,11 @@ namespace osu.Game.Beatmaps /// TextureStore LargeTextureStore { get; } + /// + /// Retrieve a global large texture store, used specifically for retrieving cropped beatmap panel backgrounds. + /// + TextureStore BeatmapPanelTextureStore { get; } + /// /// Access a global track store for retrieving beatmap tracks from. /// diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index 4b0a498a56..905bca45b3 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -49,6 +49,11 @@ namespace osu.Game.Beatmaps /// Texture GetBackground(); + /// + /// Retrieves a cropped background for this used for display on panels. + /// + Texture GetPanelBackground(); + /// /// Retrieves the for the of this . /// diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index a69859f724..25159996f3 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -66,6 +66,7 @@ namespace osu.Game.Beatmaps protected abstract IBeatmap GetBeatmap(); public abstract Texture GetBackground(); + public virtual Texture GetPanelBackground() => GetBackground(); protected abstract Track GetBeatmapTrack(); /// diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 94865ed8d0..0f3d61f527 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -42,6 +42,7 @@ namespace osu.Game.Beatmaps private readonly AudioManager audioManager; private readonly IResourceStore resources; private readonly LargeTextureStore largeTextureStore; + private readonly LargeTextureStore beatmapPanelTextureStore; private readonly ITrackStore trackStore; private readonly IResourceStore files; @@ -58,6 +59,7 @@ namespace osu.Game.Beatmaps this.host = host; this.files = files; largeTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), host?.CreateTextureLoaderStore(files)); + beatmapPanelTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), new BeatmapPanelBackgroundTextureLoaderStore(host?.CreateTextureLoaderStore(files))); this.trackStore = trackStore; } @@ -110,6 +112,7 @@ namespace osu.Game.Beatmaps #region IResourceStorageProvider TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; + TextureStore IBeatmapResourceProvider.BeatmapPanelTextureStore => beatmapPanelTextureStore; ITrackStore IBeatmapResourceProvider.Tracks => trackStore; IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer(); AudioManager IStorageResourceProvider.AudioManager => audioManager; @@ -160,7 +163,11 @@ namespace osu.Game.Beatmaps } } - public override Texture GetBackground() + public override Texture GetPanelBackground() => getBackgroundFromStore(resources.BeatmapPanelTextureStore); + + public override Texture GetBackground() => getBackgroundFromStore(resources.LargeTextureStore); + + private Texture getBackgroundFromStore(TextureStore store) { if (string.IsNullOrEmpty(Metadata?.BackgroundFile)) return null; @@ -168,7 +175,7 @@ namespace osu.Game.Beatmaps try { string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile); - var texture = resources.LargeTextureStore.Get(fileStorePath); + var texture = store.Get(fileStorePath); if (texture == null) { diff --git a/osu.Game/Screens/Select/Carousel/SetPanelBackground.cs b/osu.Game/Screens/Select/Carousel/SetPanelBackground.cs index 6f13a34bfc..b8729b7174 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelBackground.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelBackground.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; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; using osuTK; using osuTK.Graphics; @@ -21,7 +23,7 @@ namespace osu.Game.Screens.Select.Carousel Children = new Drawable[] { - new BeatmapBackgroundSprite(working) + new PanelBeatmapBackground(working) { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -68,5 +70,23 @@ namespace osu.Game.Screens.Select.Carousel }, }; } + + public partial class PanelBeatmapBackground : Sprite + { + private readonly IWorkingBeatmap working; + + public PanelBeatmapBackground(IWorkingBeatmap working) + { + ArgumentNullException.ThrowIfNull(working); + + this.working = working; + } + + [BackgroundDependencyLoader] + private void load() + { + Texture = working.GetPanelBackground(); + } + } } } From d07437f81066604178fca0748abd83eb34df2e8e Mon Sep 17 00:00:00 2001 From: John Biddle Date: Thu, 8 Jun 2023 00:52:28 -0700 Subject: [PATCH 0087/2100] Added recommendations from bdach: Fixed null checking in ApplyToDrawableHitObject Renamed mod to "Synesthesia" Moved to the "Fun" mod category --- .../Mods/{OsuModSnapColour.cs => OsuModSynesthesia.cs} | 5 ++--- osu.Game.Rulesets.Osu/OsuRuleset.cs | 4 ++-- .../Rulesets/Mods/{ModSnapColour.cs => ModSynesthesia.cs} | 8 ++++---- 3 files changed, 8 insertions(+), 9 deletions(-) rename osu.Game.Rulesets.Osu/Mods/{OsuModSnapColour.cs => OsuModSynesthesia.cs} (84%) rename osu.Game/Rulesets/Mods/{ModSnapColour.cs => ModSynesthesia.cs} (71%) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs similarity index 84% rename from osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs rename to osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs index f6fffaf736..2aec416867 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSnapColour.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs @@ -1,7 +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 osu.Framework.Extensions.ObjectExtensions; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; @@ -14,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// /// Mod that colours s based on the musical division they are on /// - public class OsuModSnapColour : ModSnapColour, IApplicableToBeatmap, IApplicableToDrawableHitObject + public class OsuModSynesthesia : ModSynesthesia, IApplicableToBeatmap, IApplicableToDrawableHitObject { private readonly OsuColour colours = new OsuColour(); @@ -29,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableHitObject(DrawableHitObject drawable) { - if (currentBeatmap.IsNull() || drawable.IsNull()) return; + if (currentBeatmap == null) return; drawable.OnUpdate += _ => drawable.AccentColour.Value = BindableBeatDivisor.GetColourFor( diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 23eea0e488..c05e640022 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -176,7 +176,6 @@ namespace osu.Game.Rulesets.Osu new OsuModClassic(), new OsuModRandom(), new OsuModMirror(), - new OsuModSnapColour(), new MultiMod(new OsuModAlternate(), new OsuModSingleTap()) }; @@ -205,7 +204,8 @@ namespace osu.Game.Rulesets.Osu new MultiMod(new OsuModMagnetised(), new OsuModRepel()), new ModAdaptiveSpeed(), new OsuModFreezeFrame(), - new OsuModBubbles() + new OsuModBubbles(), + new OsuModSynesthesia() }; case ModType.System: diff --git a/osu.Game/Rulesets/Mods/ModSnapColour.cs b/osu.Game/Rulesets/Mods/ModSynesthesia.cs similarity index 71% rename from osu.Game/Rulesets/Mods/ModSnapColour.cs rename to osu.Game/Rulesets/Mods/ModSynesthesia.cs index d7e51d8cf6..23cb135c50 100644 --- a/osu.Game/Rulesets/Mods/ModSnapColour.cs +++ b/osu.Game/Rulesets/Mods/ModSynesthesia.cs @@ -8,12 +8,12 @@ namespace osu.Game.Rulesets.Mods /// /// Mod that colours hitobjects based on the musical division they are on /// - public class ModSnapColour : Mod + public class ModSynesthesia : Mod { - public override string Name => "Snap Colour"; - public override string Acronym => "SC"; + public override string Name => "Synesthesia"; + public override string Acronym => "SY"; public override LocalisableString Description => "Colours hit objects based on the rhythm."; public override double ScoreMultiplier => 1; - public override ModType Type => ModType.Conversion; + public override ModType Type => ModType.Fun; } } From 10c43d22739b6c83006977c0d9aa92fb98372cb5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Jun 2023 17:27:00 +0900 Subject: [PATCH 0088/2100] Reduce delays and fades for carousel panels to improve song select initial display performance Entering song select has seen a hit since the new renderer implementations. The underlying cause is large numbers of vertex buffer uploads (the counter hits >200k for me during the transition). Song select is in the process of being redesigned, and we are probably going to make improvements to the renderer to alleviate this, but in the mean time we can greatly improve the user experience by reducing how long the initial fade in delays take on panels. Visually this doesn't look too jarring, and gives a more immediate feeling when scrolling. It's also more feasible to load elements sooner with https://github.com/ppy/osu/pull/23809 applied. --- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index b97d37c854..4234184ad1 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -112,11 +112,11 @@ namespace osu.Game.Screens.Select.Carousel background = new DelayedLoadWrapper(() => new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID))) { RelativeSizeAxes = Axes.Both, - }, 300) + }, 200) { RelativeSizeAxes = Axes.Both }, - mainFlow = new DelayedLoadWrapper(() => new SetPanelContent((CarouselBeatmapSet)Item), 100) + mainFlow = new DelayedLoadWrapper(() => new SetPanelContent((CarouselBeatmapSet)Item), 50) { RelativeSizeAxes = Axes.Both }, @@ -126,7 +126,7 @@ namespace osu.Game.Screens.Select.Carousel mainFlow.DelayedLoadComplete += fadeContentIn; } - private void fadeContentIn(Drawable d) => d.FadeInFromZero(750, Easing.OutQuint); + private void fadeContentIn(Drawable d) => d.FadeInFromZero(150); protected override void Deselected() { From ff4d376c84871b7b2307e7fe6d1c40bbbc17acbf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Jun 2023 21:01:05 +0900 Subject: [PATCH 0089/2100] Mark relevant components as `internal` --- osu.Game/Beatmaps/IBeatmapResourceProvider.cs | 2 +- osu.Game/Beatmaps/IWorkingBeatmap.cs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/IBeatmapResourceProvider.cs b/osu.Game/Beatmaps/IBeatmapResourceProvider.cs index d033608c95..9e79e03785 100644 --- a/osu.Game/Beatmaps/IBeatmapResourceProvider.cs +++ b/osu.Game/Beatmaps/IBeatmapResourceProvider.cs @@ -9,7 +9,7 @@ using osu.Game.IO; namespace osu.Game.Beatmaps { - public interface IBeatmapResourceProvider : IStorageResourceProvider + internal interface IBeatmapResourceProvider : IStorageResourceProvider { /// /// Retrieve a global large texture store, used for loading beatmap backgrounds. diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index 905bca45b3..dc69a7f776 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -32,12 +32,12 @@ namespace osu.Game.Beatmaps /// /// Whether the Beatmap has finished loading. /// - public bool BeatmapLoaded { get; } + bool BeatmapLoaded { get; } /// /// Whether the Track has finished loading. /// - public bool TrackLoaded { get; } + bool TrackLoaded { get; } /// /// Retrieves the which this represents. @@ -52,7 +52,7 @@ namespace osu.Game.Beatmaps /// /// Retrieves a cropped background for this used for display on panels. /// - Texture GetPanelBackground(); + internal Texture GetPanelBackground(); /// /// Retrieves the for the of this . @@ -129,12 +129,12 @@ namespace osu.Game.Beatmaps /// /// Beings loading the contents of this asynchronously. /// - public void BeginAsyncLoad(); + void BeginAsyncLoad(); /// /// Cancels the asynchronous loading of the contents of this . /// - public void CancelAsyncLoad(); + void CancelAsyncLoad(); /// /// Reads the correct track restart point from beatmap metadata and sets looping to enabled. From cccc06de4873afd0522bea33687c259b385cf8d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Jun 2023 21:10:40 +0900 Subject: [PATCH 0090/2100] Fix potential failure if beatmap background isn't tall enough --- .../Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs index 0ee516a912..ef5de4d9b2 100644 --- a/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs +++ b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs @@ -18,7 +18,7 @@ namespace osu.Game.Beatmaps public class BeatmapPanelBackgroundTextureLoaderStore : IResourceStore { // These numbers are taken from the draw visualiser size requirements for song select panel textures at extreme aspect ratios. - private const int height = 130; + private const int max_height = 130; private const int max_width = 1280; private readonly IResourceStore? textureStore; @@ -67,13 +67,14 @@ namespace osu.Game.Beatmaps Size size = image.Size(); int usableWidth = Math.Min(max_width, size.Width); + int usableHeight = Math.Min(max_height, size.Height); // Crop the centre region of the background for now. Rectangle cropRectangle = new Rectangle( (size.Width - usableWidth) / 2, - size.Height / 2 - height / 2, + (size.Height - usableHeight) / 3, usableWidth, - height + usableHeight ); image.Mutate(i => i.Crop(cropRectangle)); From 6dbf02454f0f7c2dee97aea8c01ea5120e676cff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 8 Jun 2023 14:19:34 +0200 Subject: [PATCH 0091/2100] Fix bad math --- osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs index ef5de4d9b2..6d5b90521e 100644 --- a/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs +++ b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs @@ -72,7 +72,7 @@ namespace osu.Game.Beatmaps // Crop the centre region of the background for now. Rectangle cropRectangle = new Rectangle( (size.Width - usableWidth) / 2, - (size.Height - usableHeight) / 3, + (size.Height - usableHeight) / 2, usableWidth, usableHeight ); From facf7de053a63b4155bff561b8ffede50573c561 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 8 Jun 2023 21:24:40 +0900 Subject: [PATCH 0092/2100] Parse ScoreInfo.IsLegacyScore from replays --- osu.Game/Database/RealmAccess.cs | 49 ++++++++++++++++++- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 3 ++ osu.Game/Scoring/ScoreInfo.cs | 3 +- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index f4c6c802f1..21e025c79d 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -22,12 +22,15 @@ using osu.Framework.Statistics; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Extensions; using osu.Game.Input.Bindings; +using osu.Game.IO.Legacy; using osu.Game.Models; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; using osu.Game.Skinning; using Realms; using Realms.Exceptions; @@ -72,8 +75,9 @@ namespace osu.Game.Database /// 25 2022-09-18 Remove skins to add with new naming. /// 26 2023-02-05 Added BeatmapHash to ScoreInfo. /// 27 2023-06-06 Added EditorTimestamp to BeatmapInfo. + /// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files. /// - private const int schema_version = 27; + private const int schema_version = 28; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -880,6 +884,7 @@ namespace osu.Game.Database break; case 26: + { // Add ScoreInfo.BeatmapHash property to ensure scores correspond to the correct version of beatmap. var scores = migration.NewRealm.All(); @@ -887,6 +892,33 @@ namespace osu.Game.Database score.BeatmapHash = score.BeatmapInfo.Hash; break; + } + + case 28: + { + var files = new RealmFileStore(this, storage); + var scores = migration.NewRealm.All(); + + foreach (var score in scores) + { + string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath(); + + if (replayFilename == null) + continue; + + using (var stream = files.Store.GetStream(replayFilename)) + { + if (stream == null) + continue; + + int version = new ReplayVersionParser().Parse(stream); + if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION) + score.IsLegacyScore = true; + } + } + + break; + } } } @@ -1107,5 +1139,20 @@ namespace osu.Game.Database isDisposed = true; } } + + /// + /// A trimmed down specialised to extract the version from replays. + /// + private class ReplayVersionParser + { + public int Parse(Stream stream) + { + using (SerializationReader sr = new SerializationReader(stream)) + { + sr.ReadByte(); // Ruleset. + return sr.ReadInt32(); + } + } + } } } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 9b145ad56e..c6461840aa 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -46,6 +46,9 @@ namespace osu.Game.Scoring.Legacy score.ScoreInfo = scoreInfo; int version = sr.ReadInt32(); + + scoreInfo.IsLegacyScore = version < LegacyScoreEncoder.FIRST_LAZER_VERSION; + string beatmapHash = sr.ReadString(); workingBeatmap = GetBeatmap(beatmapHash); diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index e084c45de0..d56338c6a4 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -181,8 +181,7 @@ namespace osu.Game.Scoring /// /// Whether this represents a legacy (osu!stable) score. /// - [Ignored] - public bool IsLegacyScore => Mods.OfType().Any(); + public bool IsLegacyScore { get; set; } private Dictionary? statistics; From 76df11c39843f92b4bc305b07a8ee77a08b9067a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 8 Jun 2023 21:26:32 +0900 Subject: [PATCH 0093/2100] Don't scale stable scores with the classic scoring mode --- osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs index 84bf6d15f6..52dec20b32 100644 --- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs @@ -16,7 +16,13 @@ namespace osu.Game.Scoring.Legacy => getDisplayScore(scoreProcessor.Ruleset.RulesetInfo.OnlineID, scoreProcessor.TotalScore.Value, mode, scoreProcessor.MaximumStatistics); public static long GetDisplayScore(this ScoreInfo scoreInfo, ScoringMode mode) - => getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics); + { + // Temporary to not scale stable scores that are already in the XX-millions with the classic scoring mode. + if (scoreInfo.IsLegacyScore) + return scoreInfo.TotalScore; + + return getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics); + } private static long getDisplayScore(int rulesetId, long score, ScoringMode mode, IReadOnlyDictionary maximumStatistics) { From 05bd912a21c3b18159199fdf0ac5e4038f0f69a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 8 Jun 2023 15:18:31 +0200 Subject: [PATCH 0094/2100] Revert `internal` access modifier application Unfortunately breaks a few classes (which is fixable), and also breaks Moq/Castle dynamic proxies (which is unfortunate). Relevant error: System.TypeLoadException : Method 'GetPanelBackground' in type 'Castle.Proxies.IWorkingBeatmapProxy' from assembly 'DynamicProxyGenAssembly2, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' does not have an implementation. Stack Trace: at System.Reflection.Emit.TypeBuilder.CreateTypeNoLock() at System.Reflection.Emit.TypeBuilder.CreateTypeInfo() at Castle.DynamicProxy.Generators.Emitters.AbstractTypeEmitter.BuildType() at Castle.DynamicProxy.Generators.BaseInterfaceProxyGenerator.GenerateType(String typeName, INamingScope namingScope) at Castle.DynamicProxy.Generators.BaseProxyGenerator.<>c__DisplayClass13_0.b__0(CacheKey cacheKey) at Castle.Core.Internal.SynchronizedDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory) at Castle.DynamicProxy.Generators.BaseProxyGenerator.GetProxyType() at Castle.DynamicProxy.DefaultProxyBuilder.CreateInterfaceProxyTypeWithoutTarget(Type interfaceToProxy, Type[] additionalInterfacesToProxy, ProxyGenerationOptions options) at Castle.DynamicProxy.ProxyGenerator.CreateInterfaceProxyTypeWithoutTarget(Type interfaceToProxy, Type[] additionalInterfacesToProxy, ProxyGenerationOptions options) at Castle.DynamicProxy.ProxyGenerator.CreateInterfaceProxyWithoutTarget(Type interfaceToProxy, Type[] additionalInterfacesToProxy, ProxyGenerationOptions options, IInterceptor[] interceptors) at Moq.CastleProxyFactory.CreateProxy(Type mockType, IInterceptor interceptor, Type[] interfaces, Object[] arguments) in C:\projects\moq4\src\Moq\Interception\CastleProxyFactory.cs:line 50 In theory it would be possible to fix this via application of [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] onto the `osu.Game` assembly, but I think this would be bad precedent. --- osu.Game/Beatmaps/IWorkingBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index dc69a7f776..bdfa6bdf6d 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -52,7 +52,7 @@ namespace osu.Game.Beatmaps /// /// Retrieves a cropped background for this used for display on panels. /// - internal Texture GetPanelBackground(); + Texture GetPanelBackground(); /// /// Retrieves the for the of this . From 46dc47b0b4254a9b90f00ab2b68c540b7ee94685 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Jun 2023 01:22:29 +0900 Subject: [PATCH 0095/2100] Update framework --- 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 c88bea8265..f4d08e443c 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -11,7 +11,7 @@ manifestmerger.jar - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 8a941ca6c1..b2faa7dfc2 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 1dcece7741..9aafec6c50 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -16,6 +16,6 @@ iossimulator-x64 - + From 0875fc6233475969d1bb72dbc4f3fb5bcfa98be6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Jun 2023 17:12:00 +0900 Subject: [PATCH 0096/2100] Update tests in line with new behaviour --- .../Visual/Editing/TestSceneBeatDivisorControl.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs index 3ee5ea79db..17cb3c1449 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs @@ -161,9 +161,11 @@ namespace osu.Game.Tests.Visual.Editing switchPresets(1); assertPreset(BeatDivisorType.Triplets); + assertBeatSnap(6); switchPresets(1); assertPreset(BeatDivisorType.Common); + assertBeatSnap(4); switchPresets(-1); assertPreset(BeatDivisorType.Triplets); @@ -179,6 +181,7 @@ namespace osu.Game.Tests.Visual.Editing setDivisorViaInput(15); assertPreset(BeatDivisorType.Custom, 15); + assertBeatSnap(15); switchBeatSnap(-1); assertBeatSnap(5); @@ -188,12 +191,14 @@ namespace osu.Game.Tests.Visual.Editing setDivisorViaInput(5); assertPreset(BeatDivisorType.Custom, 15); + assertBeatSnap(5); switchPresets(1); assertPreset(BeatDivisorType.Common); switchPresets(-1); - assertPreset(BeatDivisorType.Triplets); + assertPreset(BeatDivisorType.Custom, 15); + assertBeatSnap(15); } private void switchBeatSnap(int direction) => AddRepeatStep($"move snap {(direction > 0 ? "forward" : "backward")}", () => @@ -217,7 +222,7 @@ namespace osu.Game.Tests.Visual.Editing private void assertPreset(BeatDivisorType type, int? maxDivisor = null) { - AddAssert($"preset is {type}", () => bindableBeatDivisor.ValidDivisors.Value.Type == type); + AddAssert($"preset is {type}", () => bindableBeatDivisor.ValidDivisors.Value.Type, () => Is.EqualTo(type)); if (type == BeatDivisorType.Custom) { From b66d1aa33d08539cdbb0e859d8d2c2d3380be2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 8 Jun 2023 20:32:16 +0200 Subject: [PATCH 0097/2100] Fix code quality inspection --- osu.Game/Screens/Edit/EditorScreenWithTimeline.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index 84cfac8f65..b8cbff047e 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -82,7 +82,7 @@ namespace osu.Game.Screens.Edit AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Right = 5 }, }, - new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both } + new BeatDivisorControl(this.beatDivisor) { RelativeSizeAxes = Axes.Both } }, }, RowDimensions = new[] From 519923e843ea2fd2e98a9f2afa88106a48b83568 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 8 Jun 2023 17:09:20 -0700 Subject: [PATCH 0098/2100] Remove redundant `EllipsisString` assign --- osu.Game/Overlays/Chat/DrawableChatUsername.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index 4b4afc204c..46e3ff5b37 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -87,7 +87,6 @@ namespace osu.Game.Overlays.Chat { Shadow = false, Truncate = true, - EllipsisString = "…", Anchor = Anchor.TopRight, Origin = Anchor.TopRight, }; From 85fedbd02555618be2892688d2b121ae7e0c1843 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 8 Jun 2023 17:11:17 -0700 Subject: [PATCH 0099/2100] Add tooltips to truncated text --- .../Drawables/Cards/BeatmapCardExtra.cs | 9 +++------ .../Drawables/Cards/BeatmapCardNormal.cs | 6 ++---- .../Graphics/Sprites/TruncatingSpriteText.cs | 20 +++++++++++++++++++ .../Graphics/UserInterface/OsuDropdown.cs | 3 +-- .../Chat/ChannelList/ChannelListItem.cs | 3 +-- osu.Game/Overlays/Chat/ChatTextBar.cs | 3 +-- .../Overlays/Chat/DrawableChatUsername.cs | 3 +-- .../Dashboard/Home/DashboardBeatmapPanel.cs | 6 ++---- .../Play/HUD/GameplayLeaderboardScore.cs | 3 +-- .../Expanded/ExpandedPanelMiddleContent.cs | 9 +++------ osu.Game/Screens/Select/BeatmapInfoWedge.cs | 9 +++------ 11 files changed, 38 insertions(+), 36 deletions(-) create mode 100644 osu.Game/Graphics/Sprites/TruncatingSpriteText.cs diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index 5c6f0c4ee1..175c15ea7b 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -106,12 +106,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards { new Drawable[] { - new OsuSpriteText + new TruncatingSpriteText { Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Truncate = true }, titleBadgeArea = new FillFlowContainer { @@ -140,21 +139,19 @@ namespace osu.Game.Beatmaps.Drawables.Cards { new[] { - new OsuSpriteText + new TruncatingSpriteText { Text = createArtistText(), Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Truncate = true }, Empty() }, } }, - new OsuSpriteText + new TruncatingSpriteText { RelativeSizeAxes = Axes.X, - Truncate = true, Text = BeatmapSet.Source, Shadow = false, Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs index 720d892495..18e1584a98 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs @@ -107,12 +107,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards { new Drawable[] { - new OsuSpriteText + new TruncatingSpriteText { Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Truncate = true }, titleBadgeArea = new FillFlowContainer { @@ -141,12 +140,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards { new[] { - new OsuSpriteText + new TruncatingSpriteText { Text = createArtistText(), Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Truncate = true }, Empty() }, diff --git a/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs b/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs new file mode 100644 index 0000000000..da0dbd49d2 --- /dev/null +++ b/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; + +namespace osu.Game.Graphics.Sprites +{ + public sealed partial class TruncatingSpriteText : OsuSpriteText, IHasTooltip + { + public LocalisableString TooltipText => Text; + + public override bool HandlePositionalInput => IsTruncated; + + public TruncatingSpriteText() + { + Truncate = true; + } + } +} diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 3230bb0569..b530172f3e 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -335,12 +335,11 @@ namespace osu.Game.Graphics.UserInterface { new Drawable[] { - Text = new OsuSpriteText + Text = new TruncatingSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.X, - Truncate = true, }, Icon = new SpriteIcon { diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs index 57b6f6268c..21b6147113 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Chat.ChannelList new Drawable?[] { createIcon(), - text = new OsuSpriteText + text = new TruncatingSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -94,7 +94,6 @@ namespace osu.Game.Overlays.Chat.ChannelList Colour = colourProvider.Light3, Margin = new MarginPadding { Bottom = 2 }, RelativeSizeAxes = Axes.X, - Truncate = true, }, createMentionPill(), close = createCloseButton(), diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index fd5e0e9836..87e787fcb8 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -73,14 +73,13 @@ namespace osu.Game.Overlays.Chat Width = chatting_text_width, Masking = true, Padding = new MarginPadding { Horizontal = padding }, - Child = chattingText = new OsuSpriteText + Child = chattingText = new TruncatingSpriteText { MaxWidth = chatting_text_width - padding * 2, Font = OsuFont.Torus.With(size: 20), Colour = colourProvider.Background1, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Truncate = true, }, }, searchIconContainer = new Container diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index 46e3ff5b37..18632aa4af 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -83,10 +83,9 @@ namespace osu.Game.Overlays.Chat Action = openUserProfile; - drawableText = new OsuSpriteText + drawableText = new TruncatingSpriteText { Shadow = false, - Truncate = true, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, }; diff --git a/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs index 792d6cc785..f36e6b49bb 100644 --- a/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs @@ -100,17 +100,15 @@ namespace osu.Game.Overlays.Dashboard.Home Direction = FillDirection.Vertical, Children = new Drawable[] { - new OsuSpriteText + new TruncatingSpriteText { RelativeSizeAxes = Axes.X, - Truncate = true, Font = OsuFont.GetFont(weight: FontWeight.Regular), Text = BeatmapSet.Title }, - new OsuSpriteText + new TruncatingSpriteText { RelativeSizeAxes = Axes.X, - Truncate = true, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), Text = BeatmapSet.Artist }, diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 4ac2f1afda..dcb2c1071e 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -235,7 +235,7 @@ namespace osu.Game.Screens.Play.HUD } } }, - usernameText = new OsuSpriteText + usernameText = new TruncatingSpriteText { RelativeSizeAxes = Axes.X, Width = 0.6f, @@ -244,7 +244,6 @@ namespace osu.Game.Screens.Play.HUD Colour = Color4.White, Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), Text = User?.Username ?? string.Empty, - Truncate = true, Shadow = false, } } diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index f23b469f5c..fe74c1ba0d 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -101,23 +101,21 @@ namespace osu.Game.Screens.Ranking.Expanded Direction = FillDirection.Vertical, Children = new Drawable[] { - new OsuSpriteText + new TruncatingSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = new RomanisableString(metadata.TitleUnicode, metadata.Title), Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, - Truncate = true, }, - new OsuSpriteText + new TruncatingSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist), Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, - Truncate = true, }, new Container { @@ -156,14 +154,13 @@ namespace osu.Game.Screens.Ranking.Expanded AutoSizeAxes = Axes.Both, Children = new Drawable[] { - new OsuSpriteText + new TruncatingSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = beatmap.DifficultyName, Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold), MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, - Truncate = true, }, new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12)) { diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 2102df1022..961f8684ce 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -233,12 +233,11 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.X, Children = new Drawable[] { - VersionLabel = new OsuSpriteText + VersionLabel = new TruncatingSpriteText { Text = beatmapInfo.DifficultyName, Font = OsuFont.GetFont(size: 24, italics: true), RelativeSizeAxes = Axes.X, - Truncate = true, }, } }, @@ -286,19 +285,17 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.X, Children = new Drawable[] { - TitleLabel = new OsuSpriteText + TitleLabel = new TruncatingSpriteText { Current = { BindTarget = titleBinding }, Font = OsuFont.GetFont(size: 28, italics: true), RelativeSizeAxes = Axes.X, - Truncate = true, }, - ArtistLabel = new OsuSpriteText + ArtistLabel = new TruncatingSpriteText { Current = { BindTarget = artistBinding }, Font = OsuFont.GetFont(size: 17, italics: true), RelativeSizeAxes = Axes.X, - Truncate = true, }, MapperContainer = new FillFlowContainer { From 67562a38568ff7ded79ec7bacaa4faedea31de97 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 9 Jun 2023 14:35:29 +0900 Subject: [PATCH 0100/2100] Catch errors during score parsing --- osu.Game/Database/RealmAccess.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 21e025c79d..493987733b 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -911,9 +911,16 @@ namespace osu.Game.Database if (stream == null) continue; - int version = new ReplayVersionParser().Parse(stream); - if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION) - score.IsLegacyScore = true; + try + { + int version = new ReplayVersionParser().Parse(stream); + if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION) + score.IsLegacyScore = true; + } + catch (Exception e) + { + Logger.Error(e, $"Failed to read replay {replayFilename}", LoggingTarget.Database); + } } } From 1ab3b43b593fdc06e1d8b987636047824fffa128 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Jun 2023 14:36:17 +0900 Subject: [PATCH 0101/2100] Fix weird right-toolbox distance snapping display in osu!catch editor --- .../Edit/DistancedHitObjectComposer.cs | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs index a8972775de..817e8bd5fe 100644 --- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs @@ -125,6 +125,7 @@ namespace osu.Game.Rulesets.Edit if (currentSnap > DistanceSpacingMultiplier.MinValue) { currentDistanceSpacingButton.Enabled.Value = currentDistanceSpacingButton.Expanded.Value + && !DistanceSpacingMultiplier.Disabled && !Precision.AlmostEquals(currentSnap, DistanceSpacingMultiplier.Value, DistanceSpacingMultiplier.Precision / 2); currentDistanceSpacingButton.ContractedLabelText = $"current {currentSnap:N2}x"; currentDistanceSpacingButton.ExpandedLabelText = $"Use current ({currentSnap:N2}x)"; @@ -141,28 +142,31 @@ namespace osu.Game.Rulesets.Edit { base.LoadComplete(); - if (!DistanceSpacingMultiplier.Disabled) + if (DistanceSpacingMultiplier.Disabled) { - DistanceSpacingMultiplier.Value = EditorBeatmap.BeatmapInfo.DistanceSpacing; - DistanceSpacingMultiplier.BindValueChanged(multiplier => - { - distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})"; - distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({multiplier.NewValue:0.##x})"; - - if (multiplier.NewValue != multiplier.OldValue) - onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier)); - - EditorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue; - }, true); - - // Manual binding to handle enabling distance spacing when the slider is interacted with. - distanceSpacingSlider.Current.BindValueChanged(spacing => - { - DistanceSpacingMultiplier.Value = spacing.NewValue; - DistanceSnapToggle.Value = TernaryState.True; - }); - DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue); + distanceSpacingSlider.Hide(); + return; } + + DistanceSpacingMultiplier.Value = EditorBeatmap.BeatmapInfo.DistanceSpacing; + DistanceSpacingMultiplier.BindValueChanged(multiplier => + { + distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})"; + distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({multiplier.NewValue:0.##x})"; + + if (multiplier.NewValue != multiplier.OldValue) + onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier)); + + EditorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue; + }, true); + + // Manual binding to handle enabling distance spacing when the slider is interacted with. + distanceSpacingSlider.Current.BindValueChanged(spacing => + { + DistanceSpacingMultiplier.Value = spacing.NewValue; + DistanceSnapToggle.Value = TernaryState.True; + }); + DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue); } protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[] From df874b9ae8445c8daea597918c058e6fc784faa6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 9 Jun 2023 09:16:05 +0300 Subject: [PATCH 0102/2100] Fix beatmap panel background looking different than usual --- .../Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs index 6d5b90521e..8f70f8a04f 100644 --- a/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs +++ b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs @@ -66,7 +66,10 @@ namespace osu.Game.Beatmaps textureUpload.Dispose(); Size size = image.Size(); - int usableWidth = Math.Min(max_width, size.Width); + + float aspectRatio = (float)size.Width / size.Height; + + int usableWidth = Math.Min((int)(max_width * aspectRatio), size.Width); int usableHeight = Math.Min(max_height, size.Height); // Crop the centre region of the background for now. From 78b2e6f3df1a95c83e800258594eae71862f1821 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Jun 2023 15:54:22 +0900 Subject: [PATCH 0103/2100] Add setting to limit distance snapping to current time As discussed in https://github.com/ppy/osu/discussions/23815#discussioncomment-6124116. --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ osu.Game/Localisation/EditorStrings.cs | 5 +++++ .../Compose/Components/CircularDistanceSnapGrid.cs | 7 +++++++ .../Edit/Compose/Components/DistanceSnapGrid.cs | 13 +++++++++++++ osu.Game/Screens/Edit/Editor.cs | 6 ++++++ 5 files changed, 33 insertions(+) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 365ad37f4c..193068193a 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -178,6 +178,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f, 0f, 1f, 0.25f); SetDefault(OsuSetting.EditorShowHitMarkers, true); SetDefault(OsuSetting.EditorAutoSeekOnPlacement, true); + SetDefault(OsuSetting.EditorLimitedDistanceSnap, false); SetDefault(OsuSetting.LastProcessedMetadataId, -1); @@ -383,5 +384,6 @@ namespace osu.Game.Configuration SafeAreaConsiderations, ComboColourNormalisationAmount, ProfileCoverExpanded, + EditorLimitedDistanceSnap } } diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 20258b9c35..077bd92d4f 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -109,6 +109,11 @@ namespace osu.Game.Localisation /// public static LocalisableString RotationSnapped(float newRotation) => new TranslatableString(getKey(@"rotation_snapped"), @"{0:0}° (snapped)", newRotation); + /// + /// "Limit distance snap placement to current time" + /// + public static LocalisableString LimitedDistanceSnap => new TranslatableString(getKey(@"limited_distance_snap_grid"), @"Limit distance snap placement to current time"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index d6e4e1f030..2eec833832 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -18,6 +18,9 @@ namespace osu.Game.Screens.Edit.Compose.Components { public abstract partial class CircularDistanceSnapGrid : DistanceSnapGrid { + [Resolved] + private EditorClock editorClock { get; set; } + protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) : base(referenceObject, startPosition, startTime, endTime) { @@ -98,9 +101,13 @@ namespace osu.Game.Screens.Edit.Compose.Components if (travelLength < DistanceBetweenTicks) travelLength = DistanceBetweenTicks; + if (LimitedDistanceSnap.Value) + travelLength = SnapProvider.DurationToDistance(ReferenceObject, editorClock.CurrentTime - ReferenceObject.GetEndTime()); + // When interacting with the resolved snap provider, the distance spacing multiplier should first be removed // to allow for snapping at a non-multiplied ratio. float snappedDistance = SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier); + double snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance); if (snappedTime > LatestEndTime) diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 6092ebc08f..9882f6596f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -10,6 +10,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Layout; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -60,6 +61,18 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved] private BindableBeatDivisor beatDivisor { get; set; } + /// + /// When enabled, distance snap should only snap to the current time (as per the editor clock). + /// This is to emulate stable behaviour. + /// + protected Bindable LimitedDistanceSnap; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + LimitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); + } + private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); protected readonly HitObject ReferenceObject; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index bb052b1d22..a5097916cc 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -185,6 +185,7 @@ namespace osu.Game.Screens.Edit private Bindable editorBackgroundDim; private Bindable editorHitMarkers; private Bindable editorAutoSeekOnPlacement; + private Bindable editorLimitedDistanceSnap; public Editor(EditorLoader loader = null) { @@ -276,6 +277,7 @@ namespace osu.Game.Screens.Edit editorBackgroundDim = config.GetBindable(OsuSetting.EditorDim); editorHitMarkers = config.GetBindable(OsuSetting.EditorShowHitMarkers); editorAutoSeekOnPlacement = config.GetBindable(OsuSetting.EditorAutoSeekOnPlacement); + editorLimitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); AddInternal(new OsuContextMenuContainer { @@ -337,6 +339,10 @@ namespace osu.Game.Screens.Edit new ToggleMenuItem(EditorStrings.AutoSeekOnPlacement) { State = { BindTarget = editorAutoSeekOnPlacement }, + }, + new ToggleMenuItem(EditorStrings.LimitedDistanceSnap) + { + State = { BindTarget = editorLimitedDistanceSnap }, } } }, From a9071e7afd554c6e1c25fe606277e3f188700b25 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Jun 2023 17:33:59 +0900 Subject: [PATCH 0104/2100] try-catch more --- osu.Game/Database/RealmAccess.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 493987733b..048ff43f0a 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -902,25 +902,24 @@ namespace osu.Game.Database foreach (var score in scores) { string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath(); - if (replayFilename == null) continue; - using (var stream = files.Store.GetStream(replayFilename)) + try { - if (stream == null) - continue; - - try + using (var stream = files.Store.GetStream(replayFilename)) { + if (stream == null) + continue; + int version = new ReplayVersionParser().Parse(stream); if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION) score.IsLegacyScore = true; } - catch (Exception e) - { - Logger.Error(e, $"Failed to read replay {replayFilename}", LoggingTarget.Database); - } + } + catch (Exception e) + { + Logger.Error(e, $"Failed to read replay {replayFilename}", LoggingTarget.Database); } } From 53f935714ea69df4e9114427939286665b22f1c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Jun 2023 17:34:27 +0900 Subject: [PATCH 0105/2100] Inline binary reading to avoid polluting `RealmAccess` with nested class --- osu.Game/Database/RealmAccess.cs | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 048ff43f0a..50044cb8d9 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -912,9 +912,14 @@ namespace osu.Game.Database if (stream == null) continue; - int version = new ReplayVersionParser().Parse(stream); - if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION) - score.IsLegacyScore = true; + // Trimmed down logic from LegacyScoreDecoder to extract the version from replays. + using (SerializationReader sr = new SerializationReader(stream)) + { + sr.ReadByte(); // Ruleset. + int version = sr.ReadInt32(); + if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION) + score.IsLegacyScore = true; + } } } catch (Exception e) @@ -1145,20 +1150,5 @@ namespace osu.Game.Database isDisposed = true; } } - - /// - /// A trimmed down specialised to extract the version from replays. - /// - private class ReplayVersionParser - { - public int Parse(Stream stream) - { - using (SerializationReader sr = new SerializationReader(stream)) - { - sr.ReadByte(); // Ruleset. - return sr.ReadInt32(); - } - } - } } } From c2663f27a1bf742ef637a59346c018ace18126d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Jun 2023 17:58:44 +0900 Subject: [PATCH 0106/2100] Fix audio settings not displaying while watching a replay --- osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index 45b2c1b13c..064d2071ce 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -42,7 +42,8 @@ namespace osu.Game.Screens.Play.HUD //CollectionSettings = new CollectionSettings(), //DiscussionSettings = new DiscussionSettings(), PlaybackSettings = new PlaybackSettings { Expanded = { Value = false } }, - VisualSettings = new VisualSettings { Expanded = { Value = false } } + VisualSettings = new VisualSettings { Expanded = { Value = false } }, + new AudioSettings { Expanded = { Value = false } } } }; } From ca25ac446b76d7be944c7815c26ba74994bc5209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 9 Jun 2023 11:20:12 +0200 Subject: [PATCH 0107/2100] Be slightly more specific with error message --- osu.Game/Database/RealmAccess.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 50044cb8d9..63ab18db8c 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -924,7 +924,7 @@ namespace osu.Game.Database } catch (Exception e) { - Logger.Error(e, $"Failed to read replay {replayFilename}", LoggingTarget.Database); + Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database); } } From 4685ba83e1da43d2251f0da670f8a9ba85f7a55f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Jun 2023 18:24:38 +0900 Subject: [PATCH 0108/2100] Apply NRT to `MemoryCachingComponent` classes --- osu.Game/Database/MemoryCachingComponent.cs | 17 +++++----- osu.Game/Database/OnlineLookupCache.cs | 31 ++++++++----------- osu.Game/Database/UserLookupCache.cs | 10 ++---- .../Online/API/Requests/GetUsersRequest.cs | 2 -- 4 files changed, 24 insertions(+), 36 deletions(-) diff --git a/osu.Game/Database/MemoryCachingComponent.cs b/osu.Game/Database/MemoryCachingComponent.cs index 5d1a381f09..e98475efae 100644 --- a/osu.Game/Database/MemoryCachingComponent.cs +++ b/osu.Game/Database/MemoryCachingComponent.cs @@ -1,13 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Statistics; @@ -19,8 +17,9 @@ namespace osu.Game.Database /// Currently not persisted between game sessions. /// public abstract partial class MemoryCachingComponent : Component + where TLookup : notnull { - private readonly ConcurrentDictionary cache = new ConcurrentDictionary(); + private readonly ConcurrentDictionary cache = new ConcurrentDictionary(); private readonly GlobalStatistic statistics; @@ -37,12 +36,12 @@ namespace osu.Game.Database /// /// The lookup to retrieve. /// An optional to cancel the operation. - protected async Task GetAsync([NotNull] TLookup lookup, CancellationToken token = default) + protected async Task GetAsync(TLookup lookup, CancellationToken token = default) { - if (CheckExists(lookup, out TValue performance)) + if (CheckExists(lookup, out TValue? existing)) { statistics.Value.HitCount++; - return performance; + return existing; } var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false); @@ -73,7 +72,7 @@ namespace osu.Game.Database statistics.Value.Usage = cache.Count; } - protected bool CheckExists([NotNull] TLookup lookup, out TValue value) => + protected bool CheckExists(TLookup lookup, [MaybeNullWhen(false)] out TValue value) => cache.TryGetValue(lookup, out value); /// @@ -82,7 +81,7 @@ namespace osu.Game.Database /// The lookup to retrieve. /// An optional to cancel the operation. /// The computed value. - protected abstract Task ComputeValueAsync(TLookup lookup, CancellationToken token = default); + protected abstract Task ComputeValueAsync(TLookup lookup, CancellationToken token = default); private class MemoryCachingStatistics { diff --git a/osu.Game/Database/OnlineLookupCache.cs b/osu.Game/Database/OnlineLookupCache.cs index d9b37e2f29..3b54804fec 100644 --- a/osu.Game/Database/OnlineLookupCache.cs +++ b/osu.Game/Database/OnlineLookupCache.cs @@ -1,14 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Game.Online.API; @@ -21,7 +18,7 @@ namespace osu.Game.Database where TRequest : APIRequest { [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; /// /// Creates an to retrieve the values for a given collection of s. @@ -32,8 +29,7 @@ namespace osu.Game.Database /// /// Retrieves a list of s from a successful created by . /// - [CanBeNull] - protected abstract IEnumerable RetrieveResults(TRequest request); + protected abstract IEnumerable? RetrieveResults(TRequest request); /// /// Perform a lookup using the specified , populating a . @@ -41,8 +37,7 @@ namespace osu.Game.Database /// The ID to lookup. /// An optional cancellation token. /// The populated , or null if the value does not exist or the request could not be satisfied. - [ItemCanBeNull] - protected Task LookupAsync(TLookup id, CancellationToken token = default) => GetAsync(id, token); + protected Task LookupAsync(TLookup id, CancellationToken token = default) => GetAsync(id, token); /// /// Perform an API lookup on the specified , populating a . @@ -50,9 +45,9 @@ namespace osu.Game.Database /// The IDs to lookup. /// An optional cancellation token. /// The populated values. May include null results for failed retrievals. - protected Task LookupAsync(TLookup[] ids, CancellationToken token = default) + protected Task LookupAsync(TLookup[] ids, CancellationToken token = default) { - var lookupTasks = new List>(); + var lookupTasks = new List>(); foreach (var id in ids) { @@ -69,18 +64,18 @@ namespace osu.Game.Database } // cannot be sealed due to test usages (see TestUserLookupCache). - protected override async Task ComputeValueAsync(TLookup lookup, CancellationToken token = default) + protected override async Task ComputeValueAsync(TLookup lookup, CancellationToken token = default) => await queryValue(lookup).ConfigureAwait(false); - private readonly Queue<(TLookup id, TaskCompletionSource)> pendingTasks = new Queue<(TLookup, TaskCompletionSource)>(); - private Task pendingRequestTask; + private readonly Queue<(TLookup id, TaskCompletionSource)> pendingTasks = new Queue<(TLookup, TaskCompletionSource)>(); + private Task? pendingRequestTask; private readonly object taskAssignmentLock = new object(); - private Task queryValue(TLookup id) + private Task queryValue(TLookup id) { lock (taskAssignmentLock) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); // Add to the queue. pendingTasks.Enqueue((id, tcs)); @@ -96,14 +91,14 @@ namespace osu.Game.Database private async Task performLookup() { // contains at most 50 unique IDs from tasks, which is used to perform the lookup. - var nextTaskBatch = new Dictionary>>(); + var nextTaskBatch = new Dictionary>>(); // Grab at most 50 unique IDs from the queue. lock (taskAssignmentLock) { while (pendingTasks.Count > 0 && nextTaskBatch.Count < 50) { - (TLookup id, TaskCompletionSource task) next = pendingTasks.Dequeue(); + (TLookup id, TaskCompletionSource task) next = pendingTasks.Dequeue(); // Perform a secondary check for existence, in case the value was queried in a previous batch. if (CheckExists(next.id, out var existing)) @@ -113,7 +108,7 @@ namespace osu.Game.Database if (nextTaskBatch.TryGetValue(next.id, out var tasks)) tasks.Add(next.task); else - nextTaskBatch[next.id] = new List> { next.task }; + nextTaskBatch[next.id] = new List> { next.task }; } } } diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index b1609fbf7b..e581d5ce82 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -1,13 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -21,8 +18,7 @@ namespace osu.Game.Database /// The user to lookup. /// An optional cancellation token. /// The populated user, or null if the user does not exist or the request could not be satisfied. - [ItemCanBeNull] - public Task GetUserAsync(int userId, CancellationToken token = default) => LookupAsync(userId, token); + public Task GetUserAsync(int userId, CancellationToken token = default) => LookupAsync(userId, token); /// /// Perform an API lookup on the specified users, populating a model. @@ -30,10 +26,10 @@ namespace osu.Game.Database /// The users to lookup. /// An optional cancellation token. /// The populated users. May include null results for failed retrievals. - public Task GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token); + public Task GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token); protected override GetUsersRequest CreateRequest(IEnumerable ids) => new GetUsersRequest(ids.ToArray()); - protected override IEnumerable RetrieveResults(GetUsersRequest request) => request.Response?.Users; + protected override IEnumerable? RetrieveResults(GetUsersRequest request) => request.Response?.Users; } } diff --git a/osu.Game/Online/API/Requests/GetUsersRequest.cs b/osu.Game/Online/API/Requests/GetUsersRequest.cs index b57bb215aa..6f7e9c07d2 100644 --- a/osu.Game/Online/API/Requests/GetUsersRequest.cs +++ b/osu.Game/Online/API/Requests/GetUsersRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; namespace osu.Game.Online.API.Requests From c5e77e13de1c81017459dc3e14306f4fe81bd7f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Jun 2023 19:03:02 +0900 Subject: [PATCH 0109/2100] Add a very simple user cache to `ScoreImporter` --- osu.Game/Scoring/ScoreImporter.cs | 34 ++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index f69c1b9385..1c24cfbc85 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -146,16 +146,48 @@ namespace osu.Game.Scoring #pragma warning restore CS0618 } + // Very naive local caching to improve performance of large score imports (where the username is usually the same for most or all scores). + private readonly Dictionary usernameLookupCache = new Dictionary(); + protected override void PostImport(ScoreInfo model, Realm realm, ImportParameters parameters) { base.PostImport(model, realm, parameters); - var userRequest = new GetUserRequest(model.RealmUser.Username); + populateUserDetails(model); + } + + /// + /// Legacy replays only store a username. + /// This will populate a user ID during import. + /// + private void populateUserDetails(ScoreInfo model) + { + string username = model.RealmUser.Username; + + if (usernameLookupCache.TryGetValue(username, out var existing)) + { + model.User = existing; + return; + } + + var userRequest = new GetUserRequest(username); api.Perform(userRequest); if (userRequest.Response is APIUser user) + { + usernameLookupCache.TryAdd(username, new APIUser + { + // Because this is a permanent cache, let's only store the pieces we're interested in, + // rather than the full API response. If we start to store more than these three fields + // in realm, this should be undone. + Id = user.Id, + Username = user.Username, + CountryCode = user.CountryCode, + }); + model.User = user; + } } } } From 49c77a64efeb5886abe5de78aa820c7ded8a5558 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Jun 2023 19:30:28 +0900 Subject: [PATCH 0110/2100] Apply more correct fix, factoring in minimum display ratio --- ...eatmapPanelBackgroundTextureLoaderStore.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs index 8f70f8a04f..110cebbe0e 100644 --- a/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs +++ b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs @@ -17,9 +17,8 @@ namespace osu.Game.Beatmaps // If issues are found it's worth checking to make sure similar issues exist there. public class BeatmapPanelBackgroundTextureLoaderStore : IResourceStore { - // These numbers are taken from the draw visualiser size requirements for song select panel textures at extreme aspect ratios. - private const int max_height = 130; - private const int max_width = 1280; + // The aspect ratio of SetPanelBackground at its maximum size (very tall window). + private const float minimum_display_ratio = 512 / 80f; private readonly IResourceStore? textureStore; @@ -67,16 +66,18 @@ namespace osu.Game.Beatmaps Size size = image.Size(); - float aspectRatio = (float)size.Width / size.Height; - - int usableWidth = Math.Min((int)(max_width * aspectRatio), size.Width); - int usableHeight = Math.Min(max_height, size.Height); + // Assume that panel backgrounds are always displayed using `FillMode.Fill`. + // Also assume that all backgrounds are wider than they are tall, so the + // fill is always going to be based on width. + // + // We need to include enough height to make this work for all ratio panels are displayed at. + int usableHeight = (int)Math.Ceiling(size.Width * 1 / minimum_display_ratio); // Crop the centre region of the background for now. Rectangle cropRectangle = new Rectangle( - (size.Width - usableWidth) / 2, + 0, (size.Height - usableHeight) / 2, - usableWidth, + size.Width, usableHeight ); From 11694f35fe5f475e6321121c1708add60983eb70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 9 Jun 2023 13:47:35 +0200 Subject: [PATCH 0111/2100] Apply NRT in `MemoryCachingComponent` subclasses too --- osu.Game/Database/BeatmapLookupCache.cs | 10 +++------- osu.Game/Scoring/ScorePerformanceCache.cs | 9 +++------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/osu.Game/Database/BeatmapLookupCache.cs b/osu.Game/Database/BeatmapLookupCache.cs index d9bf0138dc..973c25ec4f 100644 --- a/osu.Game/Database/BeatmapLookupCache.cs +++ b/osu.Game/Database/BeatmapLookupCache.cs @@ -1,13 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -21,8 +18,7 @@ namespace osu.Game.Database /// The beatmap to lookup. /// An optional cancellation token. /// The populated beatmap, or null if the beatmap does not exist or the request could not be satisfied. - [ItemCanBeNull] - public Task GetBeatmapAsync(int beatmapId, CancellationToken token = default) => LookupAsync(beatmapId, token); + public Task GetBeatmapAsync(int beatmapId, CancellationToken token = default) => LookupAsync(beatmapId, token); /// /// Perform an API lookup on the specified beatmaps, populating a model. @@ -30,10 +26,10 @@ namespace osu.Game.Database /// The beatmaps to lookup. /// An optional cancellation token. /// The populated beatmaps. May include null results for failed retrievals. - public Task GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) => LookupAsync(beatmapIds, token); + public Task GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) => LookupAsync(beatmapIds, token); protected override GetBeatmapsRequest CreateRequest(IEnumerable ids) => new GetBeatmapsRequest(ids.ToArray()); - protected override IEnumerable RetrieveResults(GetBeatmapsRequest request) => request.Response?.Beatmaps; + protected override IEnumerable? RetrieveResults(GetBeatmapsRequest request) => request.Response?.Beatmaps; } } diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs index 17a0c0ea6a..bdbcfe4efe 100644 --- a/osu.Game/Scoring/ScorePerformanceCache.cs +++ b/osu.Game/Scoring/ScorePerformanceCache.cs @@ -1,12 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Database; @@ -21,7 +18,7 @@ namespace osu.Game.Scoring public partial class ScorePerformanceCache : MemoryCachingComponent { [Resolved] - private BeatmapDifficultyCache difficultyCache { get; set; } + private BeatmapDifficultyCache difficultyCache { get; set; } = null!; protected override bool CacheNullValues => false; @@ -30,10 +27,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(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; From 58507291b9c213e94c6ca2337f379ecc01f68899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 9 Jun 2023 13:47:50 +0200 Subject: [PATCH 0112/2100] Apply NRT in `MemoryCachingComponent` test-only subclasses --- .../Online/TestSceneCurrentlyPlayingDisplay.cs | 12 +++++------- .../SongSelect/TestSceneBeatmapMetadataDisplay.cs | 14 ++++++++------ osu.Game/Tests/Visual/TestUserLookupCache.cs | 8 +++----- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs index 4f825e1191..885c00be80 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using System.Threading; @@ -24,8 +22,8 @@ namespace osu.Game.Tests.Visual.Online { private readonly APIUser streamingUser = new APIUser { Id = 2, Username = "Test user" }; - private TestSpectatorClient spectatorClient; - private CurrentlyPlayingDisplay currentlyPlaying; + private TestSpectatorClient spectatorClient = null!; + private CurrentlyPlayingDisplay currentlyPlaying = null!; [SetUpSteps] public void SetUpSteps() @@ -88,13 +86,13 @@ namespace osu.Game.Tests.Visual.Online "pishifat" }; - protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) + protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) { // tests against failed lookups if (lookup == 13) - return Task.FromResult(null); + return Task.FromResult(null); - return Task.FromResult(new APIUser + return Task.FromResult(new APIUser { Id = lookup, Username = usernames[lookup % usernames.Length], diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs index c2537cff79..379bd838cd 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs @@ -1,10 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -24,10 +23,10 @@ namespace osu.Game.Tests.Visual.SongSelect { public partial class TestSceneBeatmapMetadataDisplay : OsuTestScene { - private BeatmapMetadataDisplay display; + private BeatmapMetadataDisplay display = null!; [Resolved] - private BeatmapManager manager { get; set; } + private BeatmapManager manager { get; set; } = null!; [Cached(typeof(BeatmapDifficultyCache))] private readonly TestBeatmapDifficultyCache testDifficultyCache = new TestBeatmapDifficultyCache(); @@ -121,7 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelect private partial class TestBeatmapDifficultyCache : BeatmapDifficultyCache { - private TaskCompletionSource calculationBlocker; + private TaskCompletionSource? calculationBlocker; private bool blockCalculation; @@ -142,10 +141,13 @@ namespace osu.Game.Tests.Visual.SongSelect } } - public override async Task GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo rulesetInfo = null, IEnumerable mods = null, CancellationToken cancellationToken = default) + public override async Task GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable? mods = null, CancellationToken cancellationToken = default) { if (blockCalculation) + { + Debug.Assert(calculationBlocker != null); await calculationBlocker.Task.ConfigureAwait(false); + } return await base.GetDifficultyAsync(beatmapInfo, rulesetInfo, mods, cancellationToken).ConfigureAwait(false); } diff --git a/osu.Game/Tests/Visual/TestUserLookupCache.cs b/osu.Game/Tests/Visual/TestUserLookupCache.cs index a3028f1a34..261e0fa75c 100644 --- a/osu.Game/Tests/Visual/TestUserLookupCache.cs +++ b/osu.Game/Tests/Visual/TestUserLookupCache.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Threading; using System.Threading.Tasks; using osu.Game.Database; @@ -18,12 +16,12 @@ namespace osu.Game.Tests.Visual /// public const int UNRESOLVED_USER_ID = -1; - protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) + protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) { if (lookup == UNRESOLVED_USER_ID) - return Task.FromResult((APIUser)null); + return Task.FromResult(null); - return Task.FromResult(new APIUser + return Task.FromResult(new APIUser { Id = lookup, Username = $"User {lookup}" From 287229efd5f2f0633510daa691b11eb982742124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 9 Jun 2023 14:25:53 +0200 Subject: [PATCH 0113/2100] Fix code quality inspection --- osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 1d496cc636..cce633d46a 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -67,7 +67,7 @@ namespace osu.Game.Online.Rooms { var beatmap = task.GetResultSafely(); - if (SelectedItem.Value?.Beatmap.OnlineID == beatmap.OnlineID) + if (beatmap != null && SelectedItem.Value?.Beatmap.OnlineID == beatmap.OnlineID) { selectedBeatmap = beatmap; beginTracking(); From 2823a62b5fe82c2232951198b73ac4253d7835f0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 9 Jun 2023 15:27:53 +0300 Subject: [PATCH 0114/2100] Avoid potential cropping error on very tall backgrounds --- osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs index 110cebbe0e..acd60b664d 100644 --- a/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs +++ b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs @@ -73,6 +73,8 @@ namespace osu.Game.Beatmaps // We need to include enough height to make this work for all ratio panels are displayed at. int usableHeight = (int)Math.Ceiling(size.Width * 1 / minimum_display_ratio); + usableHeight = Math.Min(size.Height, usableHeight); + // Crop the centre region of the background for now. Rectangle cropRectangle = new Rectangle( 0, From 52412c673d45eb9bb1cbef5d6e54644082907b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 9 Jun 2023 14:47:34 +0200 Subject: [PATCH 0115/2100] Bump replay version in encoder after Score V2 changes One release too late, but this may help in the future if we need to discern replays set with Score V2 from older lazer replays. --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index a78ae24da2..87e1e79f87 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -28,9 +28,10 @@ namespace osu.Game.Scoring.Legacy /// /// /// 30000001: Appends to the end of scores. + /// 30000002: Score stored to replay calculated using the Score V2 algorithm. /// /// - public const int LATEST_VERSION = 30000001; + public const int LATEST_VERSION = 30000002; /// /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. From c3b50e130997cd36fa440e4116c66276b5f2685d Mon Sep 17 00:00:00 2001 From: Cootz Date: Fri, 9 Jun 2023 16:25:04 +0300 Subject: [PATCH 0116/2100] Move the multiplier back to `TopRight` --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 3795ed729d..6345916e63 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -219,8 +219,8 @@ namespace osu.Game.Overlays.Mods { aboveColumnsContent.Add(multiplierDisplay = new DifficultyMultiplierDisplay { - Anchor = Anchor.Centre, - Origin = Anchor.Centre + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight }); } From 7697dbe4b3aec9ebe5788d55e0f4027f3bf4e4af Mon Sep 17 00:00:00 2001 From: Cootz Date: Fri, 9 Jun 2023 16:55:19 +0300 Subject: [PATCH 0117/2100] Mod panel don't play sound when hidden --- osu.Game/Overlays/Mods/ModColumn.cs | 4 ++-- osu.Game/Overlays/Mods/ModSelectPanel.cs | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 9146cd7abe..363f5c5d0f 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -212,8 +212,8 @@ namespace osu.Game.Overlays.Mods foreach (var button in availableMods.Where(b => b.Active.Value)) { - if (Alpha == 0f) - button.Active.Value = false; //If column is hidden change state manually without any animation + if (!button.Visible) + button.Active.Value = false; //If mod panel is hidden change state manually without any animation else pendingSelectionOperations.Enqueue(() => button.Active.Value = false); } diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index 90663d083c..9d87e1da90 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -193,6 +193,9 @@ namespace osu.Game.Overlays.Mods if (samplePlaybackDisabled.Value) return; + if (!IsPresent) + return; + if (Active.Value) sampleOn?.Play(); else From d219b5f77f33b443a3a57d91368bae74f658c7a7 Mon Sep 17 00:00:00 2001 From: Cootz Date: Fri, 9 Jun 2023 17:10:13 +0300 Subject: [PATCH 0118/2100] Reword change focus comment --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 6345916e63..574d18de0e 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -838,7 +838,8 @@ namespace osu.Game.Overlays.Mods if (!Active.Value) RequestScroll?.Invoke(this); - //Kill focus on SearchTextBox + //By doing this we kill the focus on SearchTextBox. + //Killing focus is done here because it's the only feasible place on ModSelectOverlay you can click on without triggering any action. Scheduler.Add(() => GetContainingInputManager().ChangeFocus(null)); return true; From 4b8c4bd503075fa76a375ee90fc42838338eeb69 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 10 Jun 2023 01:51:53 +0900 Subject: [PATCH 0119/2100] Fix black jaggies around argon hitcircles --- osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs index 1c5cf49625..83de50184f 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs @@ -62,7 +62,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon { outerFill = new Circle // renders white outer border and dark fill { - Size = Size, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + // Slightly inset to prevent bleeding outside the ring + Size = Size - new Vector2(0.5f), Alpha = withOuterFill ? 1 : 0, }, outerGradient = new Circle // renders the outer bright gradient From 7bf73463f1ae711066074ec704e25b711738ff4f Mon Sep 17 00:00:00 2001 From: Xinnoh <30382015+Xinnoh@users.noreply.github.com> Date: Fri, 9 Jun 2023 22:46:07 -0700 Subject: [PATCH 0120/2100] reduce argon kiai flashing --- osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs index d7e37899ce..e67417c384 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon private const double pre_beat_transition_time = 80; - private const float flash_opacity = 0.3f; + private const float flash_opacity = 0.15f; private ColourInfo accentColour; From 0c42aa7f3b19d810d2eac06803697060b179922b Mon Sep 17 00:00:00 2001 From: Xinnoh <30382015+Xinnoh@users.noreply.github.com> Date: Fri, 9 Jun 2023 22:51:45 -0700 Subject: [PATCH 0121/2100] lower kiai pulse brightness --- osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs index bde502bbed..776996fdd2 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default private const double pre_beat_transition_time = 80; - private const float flash_opacity = 0.3f; + private const float flash_opacity = 0.15f; [Resolved] private DrawableHitObject drawableHitObject { get; set; } = null!; From 9224486ec7b5c4f868438b395458b5c5328ca69a Mon Sep 17 00:00:00 2001 From: Cootz Date: Sat, 10 Jun 2023 12:38:26 +0300 Subject: [PATCH 0122/2100] Add mod select overlay statics --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 ++ .../Overlays/Mods/ModSelectOverlayStatics.cs | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 osu.Game/Overlays/Mods/ModSelectOverlayStatics.cs diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 38ae8c68cb..a46de75273 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -103,6 +103,8 @@ namespace osu.Game.Overlays.Mods private readonly BindableBool customisationVisible = new BindableBool(); + protected readonly ModSelectOverlayStatics statics = new ModSelectOverlayStatics(); + private ModSettingsArea modSettingsArea = null!; private ColumnScrollContainer columnScroll = null!; private ColumnFlowContainer columnFlow = null!; diff --git a/osu.Game/Overlays/Mods/ModSelectOverlayStatics.cs b/osu.Game/Overlays/Mods/ModSelectOverlayStatics.cs new file mode 100644 index 0000000000..91f7f2180d --- /dev/null +++ b/osu.Game/Overlays/Mods/ModSelectOverlayStatics.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Configuration; + +namespace osu.Game.Overlays.Mods +{ + public class ModSelectOverlayStatics : InMemoryConfigManager + { + protected override void InitialiseDefaults() + { + SetDefault(Static.LastModSelectPanelSoundPlaybackTime, (double?)null); + } + } + + public enum Static + { + LastModSelectPanelSoundPlaybackTime + } +} From 09cd5580e133218739c2dfde63d1871b82e435c8 Mon Sep 17 00:00:00 2001 From: Cootz Date: Sat, 10 Jun 2023 13:13:34 +0300 Subject: [PATCH 0123/2100] Add sample playback time restrictions --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 3 ++- osu.Game/Overlays/Mods/ModSelectOverlayStatics.cs | 4 ++-- osu.Game/Overlays/Mods/ModSelectPanel.cs | 11 +++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index a46de75273..f2b61e3543 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -103,7 +103,8 @@ namespace osu.Game.Overlays.Mods private readonly BindableBool customisationVisible = new BindableBool(); - protected readonly ModSelectOverlayStatics statics = new ModSelectOverlayStatics(); + [Cached] + protected readonly ModSelectOverlayStatics Statics = new ModSelectOverlayStatics(); private ModSettingsArea modSettingsArea = null!; private ColumnScrollContainer columnScroll = null!; diff --git a/osu.Game/Overlays/Mods/ModSelectOverlayStatics.cs b/osu.Game/Overlays/Mods/ModSelectOverlayStatics.cs index 91f7f2180d..d9e66d8f4f 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlayStatics.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlayStatics.cs @@ -9,12 +9,12 @@ namespace osu.Game.Overlays.Mods { protected override void InitialiseDefaults() { - SetDefault(Static.LastModSelectPanelSoundPlaybackTime, (double?)null); + SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null); } } public enum Static { - LastModSelectPanelSoundPlaybackTime + LastModSelectPanelSamplePlaybackTime } } diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index 81285833bd..42f72fbdca 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -44,6 +44,7 @@ namespace osu.Game.Overlays.Mods public const float CORNER_RADIUS = 7; public const float HEIGHT = 42; + public double SAMPLE_PLAYBACK_DELAY = 30; protected virtual float IdleSwitchWidth => 14; protected virtual float ExpandedSwitchWidth => 30; @@ -62,6 +63,9 @@ namespace osu.Game.Overlays.Mods [Resolved] protected OverlayColourProvider ColourProvider { get; private set; } = null!; + [Resolved] + protected ModSelectOverlayStatics ModOverlayStatics { get; private set; } = null!; + private readonly OsuSpriteText titleText; private readonly OsuSpriteText descriptionText; @@ -192,10 +196,17 @@ namespace osu.Game.Overlays.Mods if (samplePlaybackDisabled.Value) return; + double? lastPlaybackTime = ModOverlayStatics.Get(Static.LastModSelectPanelSamplePlaybackTime); + + if (lastPlaybackTime is not null && Time.Current - lastPlaybackTime < SAMPLE_PLAYBACK_DELAY) + return; + if (Active.Value) sampleOn?.Play(); else sampleOff?.Play(); + + ModOverlayStatics.SetValue(Static.LastModSelectPanelSamplePlaybackTime, Time.Current); } protected override bool OnHover(HoverEvent e) From 3bf900a4df9299a3cd9bcae8caa57b42372934a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 10 Jun 2023 10:30:10 +0200 Subject: [PATCH 0124/2100] Add failing test case for slider scenario --- .../Mods/TestSceneOsuModAutoplay.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs index 616a9c362d..32028823ae 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs @@ -1,22 +1,29 @@ // 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 NUnit.Framework; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods { public partial class TestSceneOsuModAutoplay : OsuModTestScene { + protected override bool AllowFail => true; + [Test] public void TestCursorPositionStoredToJudgement() { @@ -44,6 +51,36 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods FinalRate = { Value = 1.3 } }); + [Test] + public void TestPerfectScoreOnShortSliderWithRepeat() + { + AddStep("set score to standardised", () => LocalConfig.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised)); + + CreateModTest(new ModTestData + { + Autoplay = true, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Slider + { + StartTime = 500, + Position = new Vector2(256, 192), + Path = new SliderPath(new[] + { + new PathControlPoint(), + new PathControlPoint(new Vector2(0, 6.25f)) + }), + RepeatCount = 1, + SliderVelocity = 10 + } + } + }, + PassCondition = () => Player.ScoreProcessor.TotalScore.Value == 1_000_000 + }); + } + private void runSpmTest(Mod mod) { SpinnerSpmCalculator? spmCalculator = null; From 7e5533e2057a215f8bcbc9b9f1e9edc2282edcdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 10 Jun 2023 11:02:29 +0200 Subject: [PATCH 0125/2100] Fix not being able to receive full score for extremely short sliders with repeats Closes #23862. Score V2 is a scoring algorithm, which aside from the raw numerical values of each judgement, incorporates a combo component, wherein each judgement's "combo score" is derived from both the raw numerical value of the object and the current combo after the given judgement. In particular, this means that Score V2 is sensitive to the _order_ of judging objects, as if two objects with the same start time are judged using different ordering, they can end up having a different "combo score". The issue that this change is fixing is an instance of one such reordering. Upon inspection, it turned out that the simulated autoplay run, which is used to determine max possible score so that it can be standardised to 1 million again, was processing a slider repeat before a slider tail circle, while actual gameplay was processing the same slider repeat _after_ the slider tail circle. The cause of that behaviour is unfortunately due to `LegacyLastTick`. The sliders which cause the issue are extremely short. Stable had a behaviour, in which to provide leniency, slider tails were artificially offset back by 36ms. However, if the slider is not long enough to make this possible, the last tick is placed in the middle of the slider. If that slider also happens to have exactly 1 repeat, then this means that the last tick and the repeat have the same time instant. Because of the time equality, what begins to matter now is the _order_ of processing the elements of the drawable slider in the hierarchy. For the purposes of legacy skins, tail circles were moved below ticks in fce3eacd7de3254ce75619efaa2d15d59d564623 - but in this particular case, it means that the order of processing the slider elements is now inadvertently inverted, causing the entire debacle. While the fact that scoring depends on order of processing of visuals is suboptimal, there isn't a great way to address this without significant restructuring. Due to the structure of processing judgements currently in place, in which each judgement is processed independently from others by its corresponding drawable hit object, this is probably the best that can be done for the time being at least. --- .../Objects/Drawables/DrawableSlider.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 664a8146e7..01174d4d61 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -75,18 +75,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables [BackgroundDependencyLoader] private void load() { + tailContainer = new Container { RelativeSizeAxes = Axes.Both }; + AddRangeInternal(new Drawable[] { shakeContainer = new ShakeContainer { ShakeDuration = 30, RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Children = new[] { Body = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling), - tailContainer = new Container { RelativeSizeAxes = Axes.Both }, + // proxied here so that the tail is drawn under repeats/ticks - legacy skins rely on this + tailContainer.CreateProxy(), tickContainer = new Container { RelativeSizeAxes = Axes.Both }, repeatContainer = new Container { RelativeSizeAxes = Axes.Both }, + // actual tail container is placed here to ensure that tail hitobjects are processed after ticks/repeats. + // this is required for the correct operation of Score V2. + tailContainer, } }, // slider head is not included in shake as it handles hit detection, and handles its own shaking. From 31f370ec9b944ab994c3b4100b835f2a7d780fdb Mon Sep 17 00:00:00 2001 From: Cootz Date: Sat, 10 Jun 2023 13:50:45 +0300 Subject: [PATCH 0126/2100] Add comments for `ModSelectOverlayStatics` --- osu.Game/Overlays/Mods/ModSelectOverlayStatics.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlayStatics.cs b/osu.Game/Overlays/Mods/ModSelectOverlayStatics.cs index d9e66d8f4f..2d432b010f 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlayStatics.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlayStatics.cs @@ -5,6 +5,9 @@ using osu.Game.Configuration; namespace osu.Game.Overlays.Mods { + /// + /// Stores global mod overlay statics. These will not be stored after disposal of + /// public class ModSelectOverlayStatics : InMemoryConfigManager { protected override void InitialiseDefaults() @@ -15,6 +18,10 @@ namespace osu.Game.Overlays.Mods public enum Static { + /// + /// The last playback time in milliseconds of an on/off sample (from ). + /// Used to debounce on/off sounds game-wide to avoid volume saturation, especially in activating mod presets with many mods. + /// LastModSelectPanelSamplePlaybackTime } } From 502193e1c6f2cbb812c5b2f2af6392794e6af456 Mon Sep 17 00:00:00 2001 From: Cootz Date: Sat, 10 Jun 2023 13:55:37 +0300 Subject: [PATCH 0127/2100] Use debounce constant for select/deselect animation --- osu.Game/Overlays/Mods/ModColumn.cs | 2 +- osu.Game/Overlays/Mods/ModSelectPanel.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 5d9f616e5f..fe42cc0abf 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -176,7 +176,7 @@ namespace osu.Game.Overlays.Mods dequeuedAction(); // each time we play an animation, we decrease the time until the next animation (to ramp the visual and audible elements). - selectionDelay = Math.Max(30, selectionDelay * 0.8f); + selectionDelay = Math.Max(ModSelectPanel.SAMPLE_PLAYBACK_DELAY, selectionDelay * 0.8f); lastSelection = Time.Current; } else diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index 42f72fbdca..4c06378d89 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -44,7 +44,7 @@ namespace osu.Game.Overlays.Mods public const float CORNER_RADIUS = 7; public const float HEIGHT = 42; - public double SAMPLE_PLAYBACK_DELAY = 30; + public const double SAMPLE_PLAYBACK_DELAY = 30; protected virtual float IdleSwitchWidth => 14; protected virtual float ExpandedSwitchWidth => 30; From 274736b9c7fe0055c9da9c2401205d884c3ad9f7 Mon Sep 17 00:00:00 2001 From: Cootz Date: Sat, 10 Jun 2023 15:14:25 +0300 Subject: [PATCH 0128/2100] Fix missing dependency exception in unit tests --- osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs | 3 +++ osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs | 3 +++ .../Visual/UserInterface/TestSceneModPresetColumn.cs | 3 +++ osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs | 3 +++ 4 files changed, 12 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index a11000214c..08110382df 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -27,6 +27,9 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + [Cached] + private ModSelectOverlayStatics modOverlayStatics = new ModSelectOverlayStatics(); + [Resolved] private OsuConfigManager configManager { get; set; } = null!; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs index 64bdc167c2..f1283994d6 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs @@ -23,6 +23,9 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + [Cached] + private ModSelectOverlayStatics modOverlayStatics = new ModSelectOverlayStatics(); + [Test] public void TestVariousPanels() { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index 3efdba8754..3bbc24ed23 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -37,6 +37,9 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + [Cached] + private ModSelectOverlayStatics modOverlayStatics = new ModSelectOverlayStatics(); + [Cached(typeof(IDialogOverlay))] private readonly DialogOverlay dialogOverlay = new DialogOverlay(); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs index 35e352534b..f1899b9e29 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs @@ -26,6 +26,9 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + [Cached] + private ModSelectOverlayStatics modOverlayStatics = new ModSelectOverlayStatics(); + [SetUpSteps] public void SetUpSteps() { From 61a9c6fd7eb29b964a566e8224fef41c464b86c2 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 10 Jun 2023 11:53:45 -0700 Subject: [PATCH 0129/2100] Disable `Truncate` in `OsuSpriteText` Co-Authored-By: Salman Ahmed --- osu.Game/Graphics/Sprites/OsuSpriteText.cs | 7 +++++++ osu.Game/Graphics/Sprites/TruncatingSpriteText.cs | 3 ++- osu.Game/Overlays/Mods/ModSelectPanel.cs | 6 ++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/Sprites/OsuSpriteText.cs b/osu.Game/Graphics/Sprites/OsuSpriteText.cs index e149e0abfb..afbec0eab4 100644 --- a/osu.Game/Graphics/Sprites/OsuSpriteText.cs +++ b/osu.Game/Graphics/Sprites/OsuSpriteText.cs @@ -3,12 +3,19 @@ #nullable disable +using System; using osu.Framework.Graphics.Sprites; namespace osu.Game.Graphics.Sprites { public partial class OsuSpriteText : SpriteText { + [Obsolete("Use TruncatingSpriteText instead.")] + public new bool Truncate + { + set => throw new InvalidOperationException($"Use {nameof(TruncatingSpriteText)} instead."); + } + public OsuSpriteText() { Shadow = true; diff --git a/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs b/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs index da0dbd49d2..229fad29f9 100644 --- a/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs +++ b/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; namespace osu.Game.Graphics.Sprites @@ -14,7 +15,7 @@ namespace osu.Game.Graphics.Sprites public TruncatingSpriteText() { - Truncate = true; + ((SpriteText)this).Truncate = true; } } } diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index 81285833bd..6179f31637 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -118,22 +118,20 @@ namespace osu.Game.Overlays.Mods Direction = FillDirection.Vertical, Children = new[] { - titleText = new OsuSpriteText + titleText = new TruncatingSpriteText { Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Truncate = true, Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Margin = new MarginPadding { Left = -18 * ShearedOverlayContainer.SHEAR } }, - descriptionText = new OsuSpriteText + descriptionText = new TruncatingSpriteText { Font = OsuFont.Default.With(size: 12), RelativeSizeAxes = Axes.X, - Truncate = true, Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0) } } From 0d51e4f6cea0aeded84760a17156f0331c89f3bf Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 10 Jun 2023 12:24:58 -0700 Subject: [PATCH 0130/2100] Fix mod select panels having conflicting tooltips Going simple with a bool instead of making `TooltipText` init-able, as the current cases will just init `string.Empty`. And not sure if we want custom tooltip text in the future. --- osu.Game/Graphics/Sprites/TruncatingSpriteText.cs | 4 +++- osu.Game/Overlays/Mods/ModSelectPanel.cs | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs b/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs index 229fad29f9..254e708183 100644 --- a/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs +++ b/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs @@ -9,9 +9,11 @@ namespace osu.Game.Graphics.Sprites { public sealed partial class TruncatingSpriteText : OsuSpriteText, IHasTooltip { + public bool ShowTooltip { get; init; } = true; + public LocalisableString TooltipText => Text; - public override bool HandlePositionalInput => IsTruncated; + public override bool HandlePositionalInput => IsTruncated && ShowTooltip; public TruncatingSpriteText() { diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index 6179f31637..09f5e5ced7 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -126,13 +126,15 @@ namespace osu.Game.Overlays.Mods Margin = new MarginPadding { Left = -18 * ShearedOverlayContainer.SHEAR - } + }, + ShowTooltip = false, }, descriptionText = new TruncatingSpriteText { Font = OsuFont.Default.With(size: 12), RelativeSizeAxes = Axes.X, - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0) + Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + ShowTooltip = false, } } } From 4819a2879190b80c87c8822f451abb45b1d90cfc Mon Sep 17 00:00:00 2001 From: Cootz Date: Sun, 11 Jun 2023 13:53:17 +0300 Subject: [PATCH 0131/2100] Move mod overlay statics to `SessionStatics` --- .../UserInterface/TestSceneModColumn.cs | 3 --- .../Visual/UserInterface/TestSceneModPanel.cs | 3 --- .../UserInterface/TestSceneModPresetColumn.cs | 3 --- .../UserInterface/TestSceneModPresetPanel.cs | 3 --- osu.Game/Configuration/SessionStatics.cs | 8 ++++++ osu.Game/Overlays/Mods/ModSelectOverlay.cs | 3 --- .../Overlays/Mods/ModSelectOverlayStatics.cs | 27 ------------------- osu.Game/Overlays/Mods/ModSelectPanel.cs | 7 ++--- 8 files changed, 12 insertions(+), 45 deletions(-) delete mode 100644 osu.Game/Overlays/Mods/ModSelectOverlayStatics.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index 08110382df..a11000214c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -27,9 +27,6 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); - [Cached] - private ModSelectOverlayStatics modOverlayStatics = new ModSelectOverlayStatics(); - [Resolved] private OsuConfigManager configManager { get; set; } = null!; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs index f1283994d6..64bdc167c2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs @@ -23,9 +23,6 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); - [Cached] - private ModSelectOverlayStatics modOverlayStatics = new ModSelectOverlayStatics(); - [Test] public void TestVariousPanels() { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index 3bbc24ed23..3efdba8754 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -37,9 +37,6 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); - [Cached] - private ModSelectOverlayStatics modOverlayStatics = new ModSelectOverlayStatics(); - [Cached(typeof(IDialogOverlay))] private readonly DialogOverlay dialogOverlay = new DialogOverlay(); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs index f1899b9e29..35e352534b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs @@ -26,9 +26,6 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); - [Cached] - private ModSelectOverlayStatics modOverlayStatics = new ModSelectOverlayStatics(); - [SetUpSteps] public void SetUpSteps() { diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 276563e163..5e2f0c2128 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -6,6 +6,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; +using osu.Game.Overlays.Mods; namespace osu.Game.Configuration { @@ -21,6 +22,7 @@ namespace osu.Game.Configuration SetDefault(Static.LowBatteryNotificationShownOnce, false); SetDefault(Static.FeaturedArtistDisclaimerShownOnce, false); SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null); + SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null); SetDefault(Static.SeasonalBackgrounds, null); } @@ -56,5 +58,11 @@ namespace osu.Game.Configuration /// Used to debounce hover sounds game-wide to avoid volume saturation, especially in scrolling views with many UI controls like . /// LastHoverSoundPlaybackTime, + + /// + /// The last playback time in milliseconds of an on/off sample (from ). + /// Used to debounce on/off sounds game-wide to avoid volume saturation, especially in activating mod presets with many mods. + /// + LastModSelectPanelSamplePlaybackTime } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index f2b61e3543..38ae8c68cb 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -103,9 +103,6 @@ namespace osu.Game.Overlays.Mods private readonly BindableBool customisationVisible = new BindableBool(); - [Cached] - protected readonly ModSelectOverlayStatics Statics = new ModSelectOverlayStatics(); - private ModSettingsArea modSettingsArea = null!; private ColumnScrollContainer columnScroll = null!; private ColumnFlowContainer columnFlow = null!; diff --git a/osu.Game/Overlays/Mods/ModSelectOverlayStatics.cs b/osu.Game/Overlays/Mods/ModSelectOverlayStatics.cs deleted file mode 100644 index 2d432b010f..0000000000 --- a/osu.Game/Overlays/Mods/ModSelectOverlayStatics.cs +++ /dev/null @@ -1,27 +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 osu.Game.Configuration; - -namespace osu.Game.Overlays.Mods -{ - /// - /// Stores global mod overlay statics. These will not be stored after disposal of - /// - public class ModSelectOverlayStatics : InMemoryConfigManager - { - protected override void InitialiseDefaults() - { - SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null); - } - } - - public enum Static - { - /// - /// The last playback time in milliseconds of an on/off sample (from ). - /// Used to debounce on/off sounds game-wide to avoid volume saturation, especially in activating mod presets with many mods. - /// - LastModSelectPanelSamplePlaybackTime - } -} diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index 4c06378d89..be07759984 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -14,6 +14,7 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Audio; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -64,7 +65,7 @@ namespace osu.Game.Overlays.Mods protected OverlayColourProvider ColourProvider { get; private set; } = null!; [Resolved] - protected ModSelectOverlayStatics ModOverlayStatics { get; private set; } = null!; + protected SessionStatics Statics { get; private set; } = null!; private readonly OsuSpriteText titleText; private readonly OsuSpriteText descriptionText; @@ -196,7 +197,7 @@ namespace osu.Game.Overlays.Mods if (samplePlaybackDisabled.Value) return; - double? lastPlaybackTime = ModOverlayStatics.Get(Static.LastModSelectPanelSamplePlaybackTime); + double? lastPlaybackTime = Statics.Get(Static.LastModSelectPanelSamplePlaybackTime); if (lastPlaybackTime is not null && Time.Current - lastPlaybackTime < SAMPLE_PLAYBACK_DELAY) return; @@ -206,7 +207,7 @@ namespace osu.Game.Overlays.Mods else sampleOff?.Play(); - ModOverlayStatics.SetValue(Static.LastModSelectPanelSamplePlaybackTime, Time.Current); + Statics.SetValue(Static.LastModSelectPanelSamplePlaybackTime, Time.Current); } protected override bool OnHover(HoverEvent e) From 82b7e570cddc271e36360de10ee447d129733114 Mon Sep 17 00:00:00 2001 From: yhsphd Date: Sun, 11 Jun 2023 22:43:06 +0900 Subject: [PATCH 0132/2100] Add a checkbox to toggle line breaking for each mod in mappool screen --- .../Screens/MapPool/MapPoolScreen.cs | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index f0e34d78c3..fcb0c4d70b 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; using osu.Game.Tournament.Components; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; @@ -25,6 +26,7 @@ namespace osu.Game.Tournament.Screens.MapPool public partial class MapPoolScreen : TournamentMatchScreen { private readonly FillFlowContainer> mapFlows; + private TournamentMatch currentMatch; [Resolved(canBeNull: true)] private TournamentSceneManager sceneManager { get; set; } @@ -37,6 +39,8 @@ namespace osu.Game.Tournament.Screens.MapPool private readonly OsuButton buttonRedPick; private readonly OsuButton buttonBluePick; + private readonly SettingsCheckbox chkBoxLineBreak; + public MapPoolScreen() { InternalChildren = new Drawable[] @@ -98,6 +102,15 @@ namespace osu.Game.Tournament.Screens.MapPool Action = reset }, new ControlPanel.Spacer(), + new TournamentSpriteText + { + Text = "Each modpool takes" + }, + new TournamentSpriteText + { + Text = "different row" + }, + chkBoxLineBreak = new SettingsCheckbox() }, } }; @@ -107,6 +120,8 @@ namespace osu.Game.Tournament.Screens.MapPool private void load(MatchIPCInfo ipc) { ipc.Beatmap.BindValueChanged(beatmapChanged); + chkBoxLineBreak.Current.Value = true; + chkBoxLineBreak.Current.BindValueChanged(_ => rearrangeMappool()); } private void beatmapChanged(ValueChangedEvent beatmap) @@ -213,37 +228,42 @@ namespace osu.Game.Tournament.Screens.MapPool protected override void CurrentMatchChanged(ValueChangedEvent match) { base.CurrentMatchChanged(match); + currentMatch = match.NewValue; + rearrangeMappool(); + } + private void rearrangeMappool() + { mapFlows.Clear(); - if (match.NewValue == null) + if (currentMatch == null) return; - int totalRows = 0; - if (match.NewValue.Round.Value != null) + if (currentMatch.Round.Value != null) { FillFlowContainer currentFlow = null; string currentMod = null; - int flowCount = 0; - foreach (var b in match.NewValue.Round.Value.Beatmaps) + foreach (var b in currentMatch.Round.Value.Beatmaps) { if (currentFlow == null || currentMod != b.Mods) { - mapFlows.Add(currentFlow = new FillFlowContainer + if (chkBoxLineBreak.Current.Value || currentFlow == null) { - Spacing = new Vector2(10, 5), - Direction = FillDirection.Full, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - }); + mapFlows.Add(currentFlow = new FillFlowContainer + { + Spacing = new Vector2(10, 5), + Direction = FillDirection.Full, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }); + totalRows++; + flowCount = 0; + } currentMod = b.Mods; - - totalRows++; - flowCount = 0; } if (++flowCount > 2) From b986d1cee1d679fd893704e5b010a8464ab785b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jun 2023 14:18:26 +0900 Subject: [PATCH 0133/2100] Rename variables to give more context --- osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs | 4 ++-- osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs index e67417c384..cecb99c690 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon private const double pre_beat_transition_time = 80; - private const float flash_opacity = 0.15f; + private const float kiai_flash_opacity = 0.15f; private ColourInfo accentColour; @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon if (drawableHitObject.State.Value == ArmedState.Idle) { flash - .FadeTo(flash_opacity) + .FadeTo(kiai_flash_opacity) .Then() .FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs index 776996fdd2..b3833d372c 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default private const double pre_beat_transition_time = 80; - private const float flash_opacity = 0.15f; + private const float kiai_flash_opacity = 0.15f; [Resolved] private DrawableHitObject drawableHitObject { get; set; } = null!; @@ -187,7 +187,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default if (drawableHitObject.State.Value == ArmedState.Idle) { flashBox - .FadeTo(flash_opacity) + .FadeTo(kiai_flash_opacity) .Then() .FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine); } From 03a5b701e907d46861accc439a629e7778df1e79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jun 2023 14:20:54 +0900 Subject: [PATCH 0134/2100] Fix incorrect inline comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs index 83de50184f..b0492ca6df 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon InternalChildren = new Drawable[] { - outerFill = new Circle // renders white outer border and dark fill + outerFill = new Circle // renders dark fill { Anchor = Anchor.Centre, Origin = Anchor.Centre, From bddb91dc0af428928485082c5db73560d61bbfbe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jun 2023 14:23:46 +0900 Subject: [PATCH 0135/2100] Adjust adjustment to 1px based on review feedback Also split out `Size` variable for clarity --- .../Skinning/Argon/ArgonMainCirclePiece.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs index b0492ca6df..3427031dc8 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs @@ -48,12 +48,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon private Bindable configHitLighting = null!; + private static readonly Vector2 circle_size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + [Resolved] private DrawableHitObject drawableObject { get; set; } = null!; public ArgonMainCirclePiece(bool withOuterFill) { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = circle_size; Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -65,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon Anchor = Anchor.Centre, Origin = Anchor.Centre, // Slightly inset to prevent bleeding outside the ring - Size = Size - new Vector2(0.5f), + Size = circle_size - new Vector2(1), Alpha = withOuterFill ? 1 : 0, }, outerGradient = new Circle // renders the outer bright gradient @@ -91,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon Masking = true, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = Size, + Size = circle_size, Child = new KiaiFlash { RelativeSizeAxes = Axes.Both, From 1e774fc0170585b67d2a0d8b01500998a884c3f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jun 2023 14:32:55 +0900 Subject: [PATCH 0136/2100] Refactor implementation to roughly match existing `HoverSampleDebounceComponent` --- osu.Game/Overlays/Mods/ModSelectPanel.cs | 28 +++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index be07759984..be01d239b6 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -45,6 +45,7 @@ namespace osu.Game.Overlays.Mods public const float CORNER_RADIUS = 7; public const float HEIGHT = 42; + public const double SAMPLE_PLAYBACK_DELAY = 30; protected virtual float IdleSwitchWidth => 14; @@ -64,9 +65,6 @@ namespace osu.Game.Overlays.Mods [Resolved] protected OverlayColourProvider ColourProvider { get; private set; } = null!; - [Resolved] - protected SessionStatics Statics { get; private set; } = null!; - private readonly OsuSpriteText titleText; private readonly OsuSpriteText descriptionText; @@ -74,6 +72,8 @@ namespace osu.Game.Overlays.Mods private Sample? sampleOff; private Sample? sampleOn; + private Bindable lastPlaybackTime = null!; + protected ModSelectPanel() { RelativeSizeAxes = Axes.X; @@ -168,13 +168,15 @@ namespace osu.Game.Overlays.Mods protected abstract void Deselect(); [BackgroundDependencyLoader] - private void load(AudioManager audio, ISamplePlaybackDisabler? samplePlaybackDisabler) + private void load(AudioManager audio, SessionStatics statics, ISamplePlaybackDisabler? samplePlaybackDisabler) { sampleOn = audio.Samples.Get(@"UI/check-on"); sampleOff = audio.Samples.Get(@"UI/check-off"); if (samplePlaybackDisabler != null) ((IBindable)samplePlaybackDisabled).BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); + + lastPlaybackTime = statics.GetBindable(Static.LastHoverSoundPlaybackTime); } protected sealed override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet); @@ -197,17 +199,17 @@ namespace osu.Game.Overlays.Mods if (samplePlaybackDisabled.Value) return; - double? lastPlaybackTime = Statics.Get(Static.LastModSelectPanelSamplePlaybackTime); + bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= SAMPLE_PLAYBACK_DELAY; - if (lastPlaybackTime is not null && Time.Current - lastPlaybackTime < SAMPLE_PLAYBACK_DELAY) - return; + if (enoughTimePassedSinceLastPlayback) + { + if (Active.Value) + sampleOn?.Play(); + else + sampleOff?.Play(); - if (Active.Value) - sampleOn?.Play(); - else - sampleOff?.Play(); - - Statics.SetValue(Static.LastModSelectPanelSamplePlaybackTime, Time.Current); + lastPlaybackTime.Value = Time.Current; + } } protected override bool OnHover(HoverEvent e) From 8864014af84f1dcb6932d6f4573b1c9dae6d38f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jun 2023 15:25:12 +0900 Subject: [PATCH 0137/2100] Add xmldoc --- osu.Game/Graphics/Sprites/TruncatingSpriteText.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs b/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs index 254e708183..46abdbf09e 100644 --- a/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs +++ b/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs @@ -7,8 +7,14 @@ using osu.Framework.Localisation; namespace osu.Game.Graphics.Sprites { + /// + /// A derived version of which automatically shows non-truncated text in tooltip when required. + /// public sealed partial class TruncatingSpriteText : OsuSpriteText, IHasTooltip { + /// + /// Whether a tooltip should be shown with non-truncated text on hover. + /// public bool ShowTooltip { get; init; } = true; public LocalisableString TooltipText => Text; From 99f93f518555bebd6c024230b359addca0954181 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jun 2023 15:31:22 +0900 Subject: [PATCH 0138/2100] Add comment mentioning why `ShowTooltip` is disabled in mod select panels --- osu.Game/Overlays/Mods/ModSelectPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index 09f5e5ced7..d6916c49da 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -127,14 +127,14 @@ namespace osu.Game.Overlays.Mods { Left = -18 * ShearedOverlayContainer.SHEAR }, - ShowTooltip = false, + ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`. }, descriptionText = new TruncatingSpriteText { Font = OsuFont.Default.With(size: 12), RelativeSizeAxes = Axes.X, Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), - ShowTooltip = false, + ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`. } } } From c9f9569e4a6403e637cb586db2b7e21212e8e97a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jun 2023 16:22:38 +0900 Subject: [PATCH 0139/2100] Add ability to change background colour in song progress test scene --- .../Visual/Gameplay/TestSceneSongProgress.cs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs index 5855838d3c..530e4af062 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs @@ -7,12 +7,15 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; +using osuTK.Graphics; namespace osu.Game.Tests.Visual.Gameplay { @@ -21,6 +24,8 @@ namespace osu.Game.Tests.Visual.Gameplay { private GameplayClockContainer gameplayClockContainer = null!; + private Box background = null!; + private const double skip_target_time = -2000; [BackgroundDependencyLoader] @@ -30,11 +35,20 @@ namespace osu.Game.Tests.Visual.Gameplay FrameStabilityContainer frameStabilityContainer; - Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time) + AddRange(new Drawable[] { - Child = frameStabilityContainer = new FrameStabilityContainer + background = new Box { - MaxCatchUpFrames = 1 + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue + }, + gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time) + { + Child = frameStabilityContainer = new FrameStabilityContainer + { + MaxCatchUpFrames = 1 + } } }); @@ -71,6 +85,9 @@ namespace osu.Game.Tests.Visual.Gameplay applyToArgonProgress(s => s.ShowGraph.Value = b); }); + AddStep("set white background", () => background.FadeColour(Color4.White, 200, Easing.OutQuint)); + AddStep("randomise background colour", () => background.FadeColour(new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1), 200, Easing.OutQuint)); + AddStep("stop", gameplayClockContainer.Stop); } From 84670d4c909df978adc96f7864762fb6cdf80661 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jun 2023 15:52:25 +0900 Subject: [PATCH 0140/2100] Adjust argon graph to use a non-gray colour range --- .../Screens/Play/HUD/ArgonSongProgressGraph.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs index 63ab9d15e0..be570c1578 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs @@ -4,8 +4,10 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Rulesets.Objects; using osu.Game.Graphics.UserInterface; @@ -13,6 +15,10 @@ namespace osu.Game.Screens.Play.HUD { public partial class ArgonSongProgressGraph : SegmentedGraph { + private const int tier_count = 5; + + private const int display_granularity = 200; + private IEnumerable? objects; public IEnumerable Objects @@ -21,8 +27,7 @@ namespace osu.Game.Screens.Play.HUD { objects = value; - const int granularity = 200; - int[] values = new int[granularity]; + int[] values = new int[display_granularity]; if (!objects.Any()) return; @@ -32,7 +37,7 @@ namespace osu.Game.Screens.Play.HUD if (lastHit == 0) lastHit = objects.Last().StartTime; - double interval = (lastHit - firstHit + 1) / granularity; + double interval = (lastHit - firstHit + 1) / display_granularity; foreach (var h in objects) { @@ -51,12 +56,12 @@ namespace osu.Game.Screens.Play.HUD } public ArgonSongProgressGraph() - : base(5) + : base(tier_count) { var colours = new List(); - for (int i = 0; i < 5; i++) - colours.Add(Colour4.White.Darken(1 + 1 / 5f).Opacity(1 / 5f)); + for (int i = 0; i < tier_count; i++) + colours.Add(OsuColour.Gray(0.2f).Opacity(0.1f)); TierColours = colours; } From 855185ca858f9973f6f31985499c81714c811cd2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jun 2023 16:10:51 +0900 Subject: [PATCH 0141/2100] Adjust argon song progress bar's background fill to always display --- .../Screens/Play/HUD/ArgonSongProgress.cs | 1 - .../Screens/Play/HUD/ArgonSongProgressBar.cs | 34 ++++++------------- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs index 9dce8996c3..be2ce3b272 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -95,7 +95,6 @@ namespace osu.Game.Screens.Play.HUD private void updateGraphVisibility() { graph.FadeTo(ShowGraph.Value ? 1 : 0, 200, Easing.In); - bar.ShowBackground = !ShowGraph.Value; } protected override void Update() diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs index dd6e10ba5d..beaee0e9ee 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs @@ -14,7 +14,6 @@ using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Graphics; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { @@ -32,18 +31,8 @@ namespace osu.Game.Screens.Play.HUD private readonly Box background; - private readonly BindableBool showBackground = new BindableBool(); - private readonly ColourInfo mainColour; - private readonly ColourInfo mainColourDarkened; private ColourInfo catchUpColour; - private ColourInfo catchUpColourDarkened; - - public bool ShowBackground - { - get => showBackground.Value; - set => showBackground.Value = value; - } public double StartTime { @@ -95,7 +84,7 @@ namespace osu.Game.Screens.Play.HUD { RelativeSizeAxes = Axes.Both, Alpha = 0, - Colour = Colour4.White.Darken(1 + 1 / 4f) + Colour = OsuColour.Gray(0.2f), }, catchupBar = new RoundedBar { @@ -112,12 +101,10 @@ namespace osu.Game.Screens.Play.HUD Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, CornerRadius = 5, - AccentColour = mainColour = Color4.White, + AccentColour = mainColour = OsuColour.Gray(0.9f), RelativeSizeAxes = Axes.Both }, }; - - mainColourDarkened = Colour4.White.Darken(1 / 3f); } private void setupAlternateValue() @@ -141,16 +128,15 @@ namespace osu.Game.Screens.Play.HUD [BackgroundDependencyLoader] private void load(OsuColour colours) { - catchUpColour = colours.BlueLight; - catchUpColourDarkened = colours.BlueDark; - - showBackground.BindValueChanged(_ => updateBackground(), true); + catchUpColour = colours.BlueDark; } - private void updateBackground() + protected override void LoadComplete() { - background.FadeTo(showBackground.Value ? 1 / 4f : 0, 200, Easing.In); - playfieldBar.TransformTo(nameof(playfieldBar.AccentColour), ShowBackground ? mainColour : mainColourDarkened, 200, Easing.In); + base.LoadComplete(); + + background.FadeTo(0.3f, 200, Easing.In); + playfieldBar.TransformTo(nameof(playfieldBar.AccentColour), mainColour, 200, Easing.In); } protected override bool OnHover(HoverEvent e) @@ -190,8 +176,8 @@ namespace osu.Game.Screens.Play.HUD catchupBar.AccentColour = Interpolation.ValueAt( Math.Min(timeDelta, colour_transition_threshold), - ShowBackground ? mainColour : mainColourDarkened, - ShowBackground ? catchUpColour : catchUpColourDarkened, + mainColour, + catchUpColour, 0, colour_transition_threshold, Easing.OutQuint); From 062fd58602545dec493ee210daee49a2851c2ba7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jun 2023 16:25:53 +0900 Subject: [PATCH 0142/2100] Add test to known time --- osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs index 530e4af062..e975a85401 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs @@ -91,6 +91,14 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("stop", gameplayClockContainer.Stop); } + [Test] + public void TestSeekToKnownTime() + { + AddStep("seek to known time", () => gameplayClockContainer.Seek(60000)); + AddWaitStep("wait some for seek", 15); + AddStep("stop", () => gameplayClockContainer.Stop()); + } + private void applyToArgonProgress(Action action) => this.ChildrenOfType().ForEach(action); From a1b17c4468a58179d2226457d9de2c76505682c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jun 2023 17:12:18 +0900 Subject: [PATCH 0143/2100] Adjust log output for global background changes to make more sense --- osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 0d9b39f099..d9554c10e2 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Backgrounds if (nextBackground == background) return false; - Logger.Log("🌅 Background change queued"); + Logger.Log(@"🌅 Global background change queued"); cancellationTokenSource?.Cancel(); cancellationTokenSource = new CancellationTokenSource(); @@ -94,6 +94,7 @@ namespace osu.Game.Screens.Backgrounds nextTask?.Cancel(); nextTask = Scheduler.AddDelayed(() => { + Logger.Log(@"🌅 Global background loading"); LoadComponentAsync(nextBackground, displayNext, cancellationTokenSource.Token); }, 500); From a201339f9c9ab250ce10a691cda52855f4f17374 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jun 2023 17:12:38 +0900 Subject: [PATCH 0144/2100] Fix background track restarting twice when exiting song select with no active selection --- osu.Game/Screens/Select/SongSelect.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 4d6a5398c5..01c38667b1 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -814,6 +814,9 @@ namespace osu.Game.Screens.Select if (!ControlGlobalMusic) return; + if (Beatmap.Value is DummyWorkingBeatmap) + return; + ITrack track = music.CurrentTrack; bool isNewTrack = !lastTrack.TryGetTarget(out var last) || last != track; From a29f6772cd57f5194a07fb717a333bcd6d3f0b62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jun 2023 17:22:11 +0900 Subject: [PATCH 0145/2100] Fix storyboard being null if file doesn't exist It's expected that `WorkingBeatmap.Storyboard` is non-null. An example fail case is in `BeatmapBackgroundWithStoryboard.load`. --- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 0f3d61f527..78eed626f2 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -264,7 +264,7 @@ namespace osu.Game.Beatmaps if (beatmapFileStream == null) { Logger.Log($"Beatmap failed to load (file {BeatmapInfo.Path} not found on disk at expected location {fileStorePath})", level: LogLevel.Error); - return null; + return new Storyboard(); } using (var reader = new LineBufferedReader(beatmapFileStream)) From 430938fbb2c9591cb5222187e118fca4e6ea3832 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Mon, 12 Jun 2023 21:47:13 +0900 Subject: [PATCH 0146/2100] allow custom username color for chatLine user's color (e.g. green for NAT) will be ignore --- .../Online/TestSceneChatLineTruncation.cs | 8 +++++-- osu.Game/Overlays/Chat/ChatLine.cs | 22 ++++++++++++++++++- .../Overlays/Chat/DrawableChatUsername.cs | 10 ++++++++- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs index 32d95ec8dc..d816555237 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs @@ -38,10 +38,13 @@ namespace osu.Game.Tests.Visual.Online private void clear() => AddStep("clear messages", textContainer.Clear); - private void addMessageWithChecks(string text, bool isAction = false, bool isImportant = false, string username = null) + private void addMessageWithChecks(string text, bool isAction = false, bool isImportant = false, string username = null, Colour4? color = null) { int index = textContainer.Count + 1; - var newLine = new ChatLine(new DummyMessage(text, isAction, isImportant, index, username)); + var newLine = new ChatLine(new DummyMessage(text, isAction, isImportant, index, username)) + { + UsernameColour = color + }; textContainer.Add(newLine); } @@ -51,6 +54,7 @@ namespace osu.Game.Tests.Visual.Online addMessageWithChecks($"Wide {a} character username.", username: new string('w', a)); addMessageWithChecks("Short name with spaces.", username: "sho rt name"); addMessageWithChecks("Long name with spaces.", username: "long name with s p a c e s"); + addMessageWithChecks("message with custom color", username: "I have custom color", color: Colour4.Green); } private class DummyMessage : Message diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index c85206d5f7..5691a61fc4 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.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.Linq; using osu.Framework.Allocation; @@ -18,6 +19,7 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; namespace osu.Game.Overlays.Chat @@ -66,6 +68,24 @@ namespace osu.Game.Overlays.Chat private Container? highlight; + private Colour4? usernameColour; + + /// + /// if set, it will override or . + /// Must be set when constructor, otherwise throw . + /// + public Colour4? UsernameColour + { + get => usernameColour; + set + { + if (drawableUsername != null) + throw new InvalidOperationException("Can't change Username color after DrawableChatUsername created"); + + usernameColour = value; + } + } + public ChatLine(Message message) { Message = message; @@ -103,7 +123,7 @@ namespace osu.Game.Overlays.Chat Font = OsuFont.GetFont(size: FontSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true), AlwaysPresent = true, }, - drawableUsername = new DrawableChatUsername(message.Sender) + drawableUsername = new DrawableChatUsername(message.Sender, usernameColour) { Width = UsernameWidth, FontSize = FontSize, diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index 4b4afc204c..57fddf0575 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Chat private readonly Drawable colouredDrawable; - public DrawableChatUsername(APIUser user) + public DrawableChatUsername(APIUser user, Color4? customColor = null) { this.user = user; @@ -92,6 +92,14 @@ namespace osu.Game.Overlays.Chat Origin = Anchor.TopRight, }; + if (customColor != null) + { + AccentColour = customColor.Value; + + Add(colouredDrawable = drawableText); + return; + } + if (string.IsNullOrWhiteSpace(user.Colour)) { AccentColour = default_colours[user.Id % default_colours.Length]; From 2da8335da21d7ded7a4553928dd75054e84a2899 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Mon, 12 Jun 2023 21:56:37 +0900 Subject: [PATCH 0147/2100] let team member color match their team color --- .../TestSceneTournamentMatchChatDisplay.cs | 15 +++++++++++++ .../Components/TournamentMatchChatDisplay.cs | 21 ++++++++----------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs index d9ae8df651..5128add9bd 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs @@ -3,11 +3,14 @@ #nullable disable +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; +using osu.Game.Overlays.Chat; using osu.Game.Tests.Visual; using osu.Game.Tournament.Components; using osu.Game.Tournament.IPC; @@ -86,6 +89,12 @@ namespace osu.Game.Tournament.Tests.Components Content = "I am team red." })); + AddUntilStep("message from team red is red color", () => + { + var chatLine = this.ChildrenOfType().FirstOrDefault(m => m.Message.Sender.OnlineID == redUser.OnlineID); + return chatLine!.UsernameColour == TournamentGame.COLOUR_RED; + }); + AddStep("message from team red", () => testChannel.AddNewMessages(new Message(nextMessageId()) { Sender = redUser.ToAPIUser(), @@ -98,6 +107,12 @@ namespace osu.Game.Tournament.Tests.Components Content = "Not on my watch. Prepare to eat saaaaaaaaaand. Lots and lots of saaaaaaand." })); + AddUntilStep("message from team blue is blue color", () => + { + var chatLine = this.ChildrenOfType().FirstOrDefault(m => m.Message.Sender.OnlineID == blueUser.OnlineID); + return chatLine!.UsernameColour == TournamentGame.COLOUR_BLUE; + }); + AddStep("message from admin", () => testChannel.AddNewMessages(new Message(nextMessageId()) { Sender = admin, diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index 8a0dd6e336..2a2e45d70c 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -21,6 +21,9 @@ namespace osu.Game.Tournament.Components private ChannelManager manager; + [Resolved] + private LadderInfo ladderInfo { get; set; } + public TournamentMatchChatDisplay() { RelativeSizeAxes = Axes.X; @@ -71,7 +74,7 @@ namespace osu.Game.Tournament.Components public void Contract() => this.FadeOut(200); - protected override ChatLine CreateMessage(Message message) => new MatchMessage(message); + protected override ChatLine CreateMessage(Message message) => new MatchMessage(message, ladderInfo); protected override StandAloneDrawableChannel CreateDrawableChannel(Channel channel) => new MatchChannel(channel); @@ -86,19 +89,13 @@ namespace osu.Game.Tournament.Components protected partial class MatchMessage : StandAloneMessage { - public MatchMessage(Message message) + public MatchMessage(Message message, LadderInfo info) : base(message) { - } - - private void load(LadderInfo info) - { - // if (info.CurrentMatch.Value.Team1.Value.Players.Any(u => u.Id == Message.Sender.Id)) - // SenderText.Colour = TournamentGame.COLOUR_RED; - // else if (info.CurrentMatch.Value.Team2.Value.Players.Any(u => u.Id == Message.Sender.Id)) - // SenderText.Colour = TournamentGame.COLOUR_BLUE; - // else if (Message.Sender.Colour != null) - // SenderText.Colour = ColourBox.Colour = Color4Extensions.FromHex(Message.Sender.Colour); + if (info.CurrentMatch.Value.Team1.Value.Players.Any(u => u.OnlineID == Message.Sender.OnlineID)) + UsernameColour = TournamentGame.COLOUR_RED; + else if (info.CurrentMatch.Value.Team2.Value.Players.Any(u => u.OnlineID == Message.Sender.OnlineID)) + UsernameColour = TournamentGame.COLOUR_BLUE; } } } From 446807e7f67e89ee261393b1e367cead689b51bb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 12 Jun 2023 23:00:29 +0900 Subject: [PATCH 0148/2100] Add combo score / bonus score attributes --- .../Difficulty/OsuDifficultyCalculator.cs | 47 +++++++++++++++---- .../Difficulty/DifficultyAttributes.cs | 25 ++++++++-- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 5292707ac1..2095738a81 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -93,11 +93,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate; + OsuScoreV1Processor sv1Processor = new OsuScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); + return new OsuDifficultyAttributes { StarRating = starRating, Mods = mods, - LegacyTotalScore = new OsuScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods).TotalScore, AimDifficulty = aimRating, SpeedDifficulty = speedRating, SpeedNoteCount = speedNotes, @@ -110,6 +111,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty HitCircleCount = hitCirclesCount, SliderCount = sliderCount, SpinnerCount = spinnerCount, + LegacyTotalScore = sv1Processor.TotalScore, + LegacyComboScore = sv1Processor.ComboScore, + LegacyBonusScore = sv1Processor.BonusScore }; } @@ -198,20 +202,38 @@ namespace osu.Game.Rulesets.Osu.Difficulty public class OsuScoreV1Processor : ScoreV1Processor { - public int TotalScore { get; private set; } + public int TotalScore => BaseScore + ComboScore + BonusScore; + + /// + /// Amount of score that is combo-and-difficulty-multiplied, excluding mod multipliers. + /// + public int ComboScore { get; private set; } + + /// + /// Amount of score that is NOT combo-and-difficulty-multiplied. + /// + public int BaseScore { get; private set; } + + /// + /// Amount of score whose judgements would be treated as "bonus" in ScoreV2. + /// + public int BonusScore { get; private set; } + private int combo; public OsuScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) : base(baseBeatmap, playableBeatmap, mods) { foreach (var obj in playableBeatmap.HitObjects) - increaseScore(obj); + simulateHit(obj); } - private void increaseScore(HitObject hitObject) + private void simulateHit(HitObject hitObject) { bool increaseCombo = true; bool addScoreComboMultiplier = false; + bool isBonus = false; + int scoreIncrease = 0; switch (hitObject) @@ -229,11 +251,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty case SpinnerBonusTick: scoreIncrease = 1100; increaseCombo = false; + isBonus = true; break; case SpinnerTick: scoreIncrease = 100; increaseCombo = false; + isBonus = true; break; case HitCircle: @@ -243,7 +267,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty case Slider: foreach (var nested in hitObject.NestedHitObjects) - increaseScore(nested); + simulateHit(nested); scoreIncrease = 300; increaseCombo = false; @@ -267,9 +291,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty for (int i = 0; i <= totalHalfSpinsPossible; i++) { if (i > halfSpinsRequiredBeforeBonus && (i - halfSpinsRequiredBeforeBonus) % 2 == 0) - increaseScore(new SpinnerBonusTick()); + simulateHit(new SpinnerBonusTick()); else if (i > 1 && i % 2 == 0) - increaseScore(new SpinnerTick()); + simulateHit(new SpinnerTick()); } scoreIncrease = 300; @@ -280,13 +304,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (addScoreComboMultiplier) { // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) - scoreIncrease += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * ScoreMultiplier)); + ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * ScoreMultiplier)); } + if (isBonus) + BonusScore += scoreIncrease; + else + BaseScore += scoreIncrease; + if (increaseCombo) combo++; - - TotalScore += scoreIncrease; } } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 8e30050a7f..5a51fb24a6 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Difficulty { @@ -27,6 +28,8 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_SLIDER_FACTOR = 19; protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21; protected const int ATTRIB_ID_LEGACY_TOTAL_SCORE = 23; + protected const int ATTRIB_ID_LEGACY_COMBO_SCORE = 25; + protected const int ATTRIB_ID_LEGACY_BONUS_SCORE = 27; /// /// The mods which were applied to the beatmap. @@ -36,21 +39,33 @@ namespace osu.Game.Rulesets.Difficulty /// /// The combined star rating of all skills. /// - [JsonProperty("star_rating", Order = -4)] + [JsonProperty("star_rating", Order = -7)] public double StarRating { get; set; } /// /// The maximum achievable combo. /// - [JsonProperty("max_combo", Order = -3)] + [JsonProperty("max_combo", Order = -6)] public int MaxCombo { get; set; } /// /// The maximum achievable legacy total score. /// - [JsonProperty("legacy_total_score", Order = -2)] + [JsonProperty("legacy_total_score", Order = -5)] public int LegacyTotalScore { get; set; } + /// + /// The combo-multiplied portion of . + /// + [JsonProperty("legacy_combo_score", Order = -4)] + public int LegacyComboScore { get; set; } + + /// + /// The "bonus" portion of consisting of all judgements that would be or . + /// + [JsonProperty("legacy_bonus_score", Order = -3)] + public int LegacyBonusScore { get; set; } + /// /// Creates new . /// @@ -79,6 +94,8 @@ namespace osu.Game.Rulesets.Difficulty { yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); yield return (ATTRIB_ID_LEGACY_TOTAL_SCORE, LegacyTotalScore); + yield return (ATTRIB_ID_LEGACY_COMBO_SCORE, LegacyComboScore); + yield return (ATTRIB_ID_LEGACY_BONUS_SCORE, LegacyBonusScore); } /// @@ -90,6 +107,8 @@ namespace osu.Game.Rulesets.Difficulty { MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; LegacyTotalScore = (int)values[ATTRIB_ID_LEGACY_TOTAL_SCORE]; + LegacyComboScore = (int)values[ATTRIB_ID_LEGACY_COMBO_SCORE]; + LegacyBonusScore = (int)values[ATTRIB_ID_LEGACY_BONUS_SCORE]; } } } From 27b99ea923a590a09dea50905031daa5a7a112bb Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Tue, 13 Jun 2023 00:39:25 +0900 Subject: [PATCH 0149/2100] use `{ get; init; }` --- .../TestSceneTournamentMatchChatDisplay.cs | 10 +--- osu.Game/Overlays/Chat/ChatLine.cs | 49 ++++++++++--------- .../Overlays/Chat/DrawableChatUsername.cs | 12 +---- 3 files changed, 29 insertions(+), 42 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs index 5128add9bd..fada340cf7 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs @@ -90,10 +90,7 @@ namespace osu.Game.Tournament.Tests.Components })); AddUntilStep("message from team red is red color", () => - { - var chatLine = this.ChildrenOfType().FirstOrDefault(m => m.Message.Sender.OnlineID == redUser.OnlineID); - return chatLine!.UsernameColour == TournamentGame.COLOUR_RED; - }); + this.ChildrenOfType().Any(s => s.AccentColour == TournamentGame.COLOUR_RED)); AddStep("message from team red", () => testChannel.AddNewMessages(new Message(nextMessageId()) { @@ -108,10 +105,7 @@ namespace osu.Game.Tournament.Tests.Components })); AddUntilStep("message from team blue is blue color", () => - { - var chatLine = this.ChildrenOfType().FirstOrDefault(m => m.Message.Sender.OnlineID == blueUser.OnlineID); - return chatLine!.UsernameColour == TournamentGame.COLOUR_BLUE; - }); + this.ChildrenOfType().Any(s => s.AccentColour == TournamentGame.COLOUR_BLUE)); AddStep("message from admin", () => testChannel.AddNewMessages(new Message(nextMessageId()) { diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 5691a61fc4..3e484d962b 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -1,7 +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.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -21,6 +20,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; +using osuTK.Graphics; namespace osu.Game.Overlays.Chat { @@ -68,23 +68,10 @@ namespace osu.Game.Overlays.Chat private Container? highlight; - private Colour4? usernameColour; - /// /// if set, it will override or . - /// Must be set when constructor, otherwise throw . /// - public Colour4? UsernameColour - { - get => usernameColour; - set - { - if (drawableUsername != null) - throw new InvalidOperationException("Can't change Username color after DrawableChatUsername created"); - - usernameColour = value; - } - } + public Color4? UsernameColour { get; init; } public ChatLine(Message message) { @@ -100,6 +87,28 @@ namespace osu.Game.Overlays.Chat configManager.BindWith(OsuSetting.Prefer24HourTime, prefer24HourTime); prefer24HourTime.BindValueChanged(_ => updateTimestamp()); + if (UsernameColour != null) + { + drawableUsername = new DrawableChatUsername(message.Sender) + { + AccentColour = UsernameColour.Value + }; + } + else + { + drawableUsername = new DrawableChatUsername(message.Sender); + } + + drawableUsername.With(u => + { + u.Width = UsernameWidth; + u.FontSize = FontSize; + u.AutoSizeAxes = Axes.Y; + u.Origin = Anchor.TopRight; + u.Anchor = Anchor.TopRight; + u.Margin = new MarginPadding { Horizontal = Spacing }; + }); + InternalChild = new GridContainer { RelativeSizeAxes = Axes.X, @@ -123,15 +132,7 @@ namespace osu.Game.Overlays.Chat Font = OsuFont.GetFont(size: FontSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true), AlwaysPresent = true, }, - drawableUsername = new DrawableChatUsername(message.Sender, usernameColour) - { - Width = UsernameWidth, - FontSize = FontSize, - AutoSizeAxes = Axes.Y, - Origin = Anchor.TopRight, - Anchor = Anchor.TopRight, - Margin = new MarginPadding { Horizontal = Spacing }, - }, + drawableUsername, drawableContentFlow = new LinkFlowContainer(styleMessageContent) { AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index 57fddf0575..9b802a8070 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Chat { public Action? ReportRequested; - public Color4 AccentColour { get; } + public Color4 AccentColour { get; init; } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => colouredDrawable.ReceivePositionalInputAt(screenSpacePos); @@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Chat private readonly Drawable colouredDrawable; - public DrawableChatUsername(APIUser user, Color4? customColor = null) + public DrawableChatUsername(APIUser user) { this.user = user; @@ -92,14 +92,6 @@ namespace osu.Game.Overlays.Chat Origin = Anchor.TopRight, }; - if (customColor != null) - { - AccentColour = customColor.Value; - - Add(colouredDrawable = drawableText); - return; - } - if (string.IsNullOrWhiteSpace(user.Colour)) { AccentColour = default_colours[user.Id % default_colours.Length]; From f30c1a564fa074b4ffc1167cb34acdd35b4e68e7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Jun 2023 01:08:51 +0900 Subject: [PATCH 0150/2100] Add basic setup for score migration --- osu.Game/Database/RealmAccess.cs | 50 ++++++++- .../StandardisedScoreMigrationTools.cs | 100 ++++++++++++++++++ 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Database/StandardisedScoreMigrationTools.cs diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 63ab18db8c..4c55a408c4 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -28,7 +28,10 @@ using osu.Game.IO.Legacy; using osu.Game.Models; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using osu.Game.Skinning; @@ -76,8 +79,9 @@ namespace osu.Game.Database /// 26 2023-02-05 Added BeatmapHash to ScoreInfo. /// 27 2023-06-06 Added EditorTimestamp to BeatmapInfo. /// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files. + /// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes. /// - private const int schema_version = 28; + private const int schema_version = 29; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -930,6 +934,28 @@ namespace osu.Game.Database break; } + + case 29: + { + var scores = migration.NewRealm + .All() + .Where(s => !s.IsLegacyScore); + + foreach (var score in scores) + { + // Recalculate the old-style standardised score to see if this was an old lazer score. + long oldStandardised = StandardisedScoreMigrationTools.GetOldStandardised(score); + + if (oldStandardised == score.TotalScore) + { + long calculatedNew = StandardisedScoreMigrationTools.GetNewStandardised(score); + Logger.Log($"Converting score {score.Rank} {score.Accuracy:P1} {score.TotalScore} -> {calculatedNew}"); + score.TotalScore = calculatedNew; + } + } + + break; + } } } @@ -1151,4 +1177,26 @@ namespace osu.Game.Database } } } + + internal class FakeHit : HitObject + { + private readonly Judgement judgement; + + public override Judgement CreateJudgement() => judgement; + + public FakeHit(Judgement judgement) + { + this.judgement = judgement; + } + } + + internal class FakeJudgement : Judgement + { + public override HitResult MaxResult { get; } + + public FakeJudgement(HitResult result) + { + MaxResult = result; + } + } } diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs new file mode 100644 index 0000000000..3133095df6 --- /dev/null +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -0,0 +1,100 @@ +// 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.Diagnostics; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; + +namespace osu.Game.Database +{ + public static class StandardisedScoreMigrationTools + { + public static long GetNewStandardised(ScoreInfo score) + { + var processor = score.Ruleset.CreateInstance().CreateScoreProcessor(); + + var beatmap = new Beatmap(); + + var maximumJudgements = score.MaximumStatistics + .Where(kvp => kvp.Key.AffectsCombo()) + .OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key)) + .SelectMany(kvp => Enumerable.Repeat(new FakeJudgement(kvp.Key), kvp.Value)) + .ToList(); + + // This is a list of all results, ordered from best to worst. + // We are constructing a "best possible" score from the statistics provided because it's the best we can do. + List sortedHits = score.Statistics + .Where(kvp => kvp.Key.AffectsCombo() && kvp.Key != HitResult.Miss && kvp.Key != HitResult.LargeTickMiss) + .OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key)) + .SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value)) + .ToList(); + + foreach (var judgement in maximumJudgements) + beatmap.HitObjects.Add(new FakeHit(judgement)); + + processor.ApplyBeatmap(beatmap); + + Queue misses = new Queue(score.Statistics + .Where(kvp => kvp.Key == HitResult.Miss || kvp.Key == HitResult.LargeTickMiss) + .SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value))); + + int maxJudgementIndex = 0; + + foreach (var result in sortedHits) + { + if (processor.Combo.Value == score.MaxCombo) + { + processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++]) + { + Type = misses.Dequeue(), + }); + } + + // TODO: pass a Judgement with correct MaxResult + processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++]) + { + Type = result + }); + } + + var bonusHits = score.Statistics + .Where(kvp => kvp.Key.IsBonus()) + .SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value)); + + foreach (var result in bonusHits) + processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(result)) { Type = result }); + + Debug.Assert(processor.HighestCombo.Value == score.MaxCombo); + + return processor.TotalScore.Value; + } + + public static long GetOldStandardised(ScoreInfo score) + { + double accuracyScore = + (double)score.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value) + / score.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value); + double comboScore = (double)score.MaxCombo / score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value); + double bonusScore = score.Statistics.Where(kvp => kvp.Key.IsBonus()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value); + + double accuracyPortion = 0.3; + + switch (score.RulesetID) + { + case 1: + accuracyPortion = 0.75; + break; + + case 3: + accuracyPortion = 0.99; + break; + } + + return (long)(1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore); + } + } +} From d19f8997fc2288df0ec333fca0369be974f0c0d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Jun 2023 01:39:38 +0900 Subject: [PATCH 0151/2100] Account for scores which don't have correct maximum statistics populated --- .../StandardisedScoreMigrationTools.cs | 65 +++++++++++++++++-- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 3133095df6..e8f1fd640b 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -15,10 +15,13 @@ namespace osu.Game.Database { public static long GetNewStandardised(ScoreInfo score) { - var processor = score.Ruleset.CreateInstance().CreateScoreProcessor(); + var ruleset = score.Ruleset.CreateInstance(); + var processor = ruleset.CreateScoreProcessor(); var beatmap = new Beatmap(); + HitResult maxRulesetJudgement = ruleset.GetHitResults().First().result; + var maximumJudgements = score.MaximumStatistics .Where(kvp => kvp.Key.AffectsCombo()) .OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key)) @@ -28,11 +31,20 @@ namespace osu.Game.Database // This is a list of all results, ordered from best to worst. // We are constructing a "best possible" score from the statistics provided because it's the best we can do. List sortedHits = score.Statistics - .Where(kvp => kvp.Key.AffectsCombo() && kvp.Key != HitResult.Miss && kvp.Key != HitResult.LargeTickMiss) + .Where(kvp => kvp.Key.AffectsCombo()) .OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key)) .SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value)) .ToList(); + if (maximumJudgements.Count != sortedHits.Count) + { + // Older scores may not have maximum judgements populated correctly. + // In this case we need to fill them. + maximumJudgements = sortedHits + .Select(r => new FakeJudgement(getMaxJudgementFor(r, maxRulesetJudgement))) + .ToList(); + } + foreach (var judgement in maximumJudgements) beatmap.HitObjects.Add(new FakeHit(judgement)); @@ -46,15 +58,29 @@ namespace osu.Game.Database foreach (var result in sortedHits) { + // misses are handled from the queue. + if (result == HitResult.Miss || result == HitResult.LargeTickMiss) + continue; + if (processor.Combo.Value == score.MaxCombo) { - processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++]) + if (misses.Count > 0) { - Type = misses.Dequeue(), - }); + processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++]) + { + Type = misses.Dequeue(), + }); + } + else + { + // worst case scenario, insert a miss. + processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(getMaxJudgementFor(HitResult.Miss, maxRulesetJudgement))) + { + Type = HitResult.Miss, + }); + } } - // TODO: pass a Judgement with correct MaxResult processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++]) { Type = result @@ -68,11 +94,36 @@ namespace osu.Game.Database foreach (var result in bonusHits) processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(result)) { Type = result }); - Debug.Assert(processor.HighestCombo.Value == score.MaxCombo); + // Not true for all scores for whatever reason. Oh well. + // Debug.Assert(processor.HighestCombo.Value == score.MaxCombo); return processor.TotalScore.Value; } + private static HitResult getMaxJudgementFor(HitResult hitResult, HitResult max) + { + switch (hitResult) + { + case HitResult.Miss: + case HitResult.Meh: + case HitResult.Ok: + case HitResult.Good: + case HitResult.Great: + case HitResult.Perfect: + return max; + + case HitResult.SmallTickMiss: + case HitResult.SmallTickHit: + return HitResult.SmallTickHit; + + case HitResult.LargeTickMiss: + case HitResult.LargeTickHit: + return HitResult.LargeTickHit; + } + + return HitResult.IgnoreHit; + } + public static long GetOldStandardised(ScoreInfo score) { double accuracyScore = From 0e9576acfb0d282dbde2046a3cdcfbe7e840efe3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Jun 2023 01:40:32 +0900 Subject: [PATCH 0152/2100] Remove logging and catch any kind of errors --- osu.Game/Database/RealmAccess.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 4c55a408c4..070738363a 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -944,13 +944,20 @@ namespace osu.Game.Database foreach (var score in scores) { // Recalculate the old-style standardised score to see if this was an old lazer score. - long oldStandardised = StandardisedScoreMigrationTools.GetOldStandardised(score); + bool oldScoreMatchesExpectations = StandardisedScoreMigrationTools.GetOldStandardised(score) == score.TotalScore; + // Some older score don't have correct statistics populated, so let's give them benefit of doubt. + bool scoreIsVeryOld = score.Date < new DateTime(2023, 1, 1, 0, 0, 0); - if (oldStandardised == score.TotalScore) + if (oldScoreMatchesExpectations || scoreIsVeryOld) { - long calculatedNew = StandardisedScoreMigrationTools.GetNewStandardised(score); - Logger.Log($"Converting score {score.Rank} {score.Accuracy:P1} {score.TotalScore} -> {calculatedNew}"); - score.TotalScore = calculatedNew; + try + { + long calculatedNew = StandardisedScoreMigrationTools.GetNewStandardised(score); + score.TotalScore = calculatedNew; + } + catch + { + } } } From 0916ae1671b18cfba735f3804cbc56ecd57983a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Jun 2023 01:48:32 +0900 Subject: [PATCH 0153/2100] Add basic profiling output of realm migrations --- osu.Game/Database/RealmAccess.cs | 8 ++++++++ osu.Game/Database/StandardisedScoreMigrationTools.cs | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 070738363a..cea307f255 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -37,6 +37,7 @@ using osu.Game.Scoring.Legacy; using osu.Game.Skinning; using Realms; using Realms.Exceptions; +using Stopwatch = System.Diagnostics.Stopwatch; namespace osu.Game.Database { @@ -728,6 +729,11 @@ namespace osu.Game.Database private void applyMigrationsForVersion(Migration migration, ulong targetVersion) { + Logger.Log($"Running realm migration to version {targetVersion}..."); + Stopwatch stopwatch = new Stopwatch(); + + stopwatch.Start(); + switch (targetVersion) { case 7: @@ -964,6 +970,8 @@ namespace osu.Game.Database break; } } + + Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); } private string? getRulesetShortNameFromLegacyID(long rulesetId) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index e8f1fd640b..f103ded6d5 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; From 87520ae4000ec34efbc22560d5566c8602acc782 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Jun 2023 02:05:00 +0900 Subject: [PATCH 0154/2100] Avoid overhead from retrieving `MaxCombo` inside loop from realm --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index f103ded6d5..b621b67cf8 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -14,6 +14,9 @@ namespace osu.Game.Database { public static long GetNewStandardised(ScoreInfo score) { + // Avoid retrieving from realm inside loops. + int maxCombo = score.MaxCombo; + var ruleset = score.Ruleset.CreateInstance(); var processor = ruleset.CreateScoreProcessor(); @@ -61,7 +64,7 @@ namespace osu.Game.Database if (result == HitResult.Miss || result == HitResult.LargeTickMiss) continue; - if (processor.Combo.Value == score.MaxCombo) + if (processor.Combo.Value == maxCombo) { if (misses.Count > 0) { From e0ebb000d697aebeab253bf810c371d020b7ec31 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Jun 2023 02:05:11 +0900 Subject: [PATCH 0155/2100] Avoid unnecessary operations during score processor simulation --- osu.Game/Rulesets/Scoring/JudgementProcessor.cs | 9 +++++++++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 9 ++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 09b5f0a6bc..b16c307206 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.Scoring /// protected int MaxHits { get; private set; } + /// + /// Whether is currently running. + /// + protected bool IsSimulating { get; private set; } + /// /// The total number of judged s at the current point in time. /// @@ -146,6 +151,8 @@ namespace osu.Game.Rulesets.Scoring /// The to simulate. protected virtual void SimulateAutoplay(IBeatmap beatmap) { + IsSimulating = true; + foreach (var obj in beatmap.HitObjects) simulate(obj); @@ -163,6 +170,8 @@ namespace osu.Game.Rulesets.Scoring result.Type = GetSimulatedHitResult(judgement); ApplyResult(result); } + + IsSimulating = false; } protected override void Update() diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index ac17de32d8..87f2b1e5ee 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -226,10 +226,13 @@ namespace osu.Game.Rulesets.Scoring ApplyScoreChange(result); - hitEvents.Add(CreateHitEvent(result)); - lastHitObject = result.HitObject; + if (!IsSimulating) + { + hitEvents.Add(CreateHitEvent(result)); + lastHitObject = result.HitObject; - updateScore(); + updateScore(); + } } /// From 385f6dbd84f3af5d76d984f09a489e5b44da15f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Jun 2023 02:12:23 +0900 Subject: [PATCH 0156/2100] Move local classes to their rightful location --- osu.Game/Database/RealmAccess.cs | 22 ------------------ .../StandardisedScoreMigrationTools.cs | 23 +++++++++++++++++++ 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index cea307f255..1194e1d9f8 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -1192,26 +1192,4 @@ namespace osu.Game.Database } } } - - internal class FakeHit : HitObject - { - private readonly Judgement judgement; - - public override Judgement CreateJudgement() => judgement; - - public FakeHit(Judgement judgement) - { - this.judgement = judgement; - } - } - - internal class FakeJudgement : Judgement - { - public override HitResult MaxResult { get; } - - public FakeJudgement(HitResult result) - { - MaxResult = result; - } - } } diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index b621b67cf8..ec08168f7c 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -149,5 +150,27 @@ namespace osu.Game.Database return (long)(1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore); } + + private class FakeHit : HitObject + { + private readonly Judgement judgement; + + public override Judgement CreateJudgement() => judgement; + + public FakeHit(Judgement judgement) + { + this.judgement = judgement; + } + } + + private class FakeJudgement : Judgement + { + public override HitResult MaxResult { get; } + + public FakeJudgement(HitResult result) + { + MaxResult = result; + } + } } } From e9fb1f89326d549ee6f67e457daa471f1ee23467 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Jun 2023 02:15:08 +0900 Subject: [PATCH 0157/2100] Avoid tracking hit events altogether during migration --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 2 ++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index ec08168f7c..c06502b521 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -21,6 +21,8 @@ namespace osu.Game.Database var ruleset = score.Ruleset.CreateInstance(); var processor = ruleset.CreateScoreProcessor(); + processor.TrackHitEvents = false; + var beatmap = new Beatmap(); HitResult maxRulesetJudgement = ruleset.GetHitResults().First().result; diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 87f2b1e5ee..f29e3533a0 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.Scoring private const double accuracy_cutoff_c = 0.7; private const double accuracy_cutoff_d = 0; + /// + /// Whether should be populated during application of results. + /// + internal bool TrackHitEvents = true; + /// /// Invoked when this was reset from a replay frame. /// @@ -226,7 +231,7 @@ namespace osu.Game.Rulesets.Scoring ApplyScoreChange(result); - if (!IsSimulating) + if (!IsSimulating && TrackHitEvents) { hitEvents.Add(CreateHitEvent(result)); lastHitObject = result.HitObject; From b9f485b551f672c25721cbd28d9124f8f6fe1b6d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 12 Jun 2023 23:05:09 +0900 Subject: [PATCH 0158/2100] Merge classes + split out --- .../Difficulty/OsuDifficultyCalculator.cs | 164 ----------------- .../Difficulty/OsuScoreV1Processor.cs | 167 ++++++++++++++++++ 2 files changed, 167 insertions(+), 164 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 2095738a81..21ee03d1a5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -11,8 +11,6 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; @@ -154,166 +152,4 @@ namespace osu.Game.Rulesets.Osu.Difficulty new MultiMod(new OsuModFlashlight(), new OsuModHidden()) }; } - - public abstract class ScoreV1Processor - { - protected readonly int DifficultyPeppyStars; - protected readonly double ScoreMultiplier; - - protected readonly IBeatmap PlayableBeatmap; - - protected ScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) - { - PlayableBeatmap = playableBeatmap; - - int countNormal = 0; - int countSlider = 0; - int countSpinner = 0; - - foreach (HitObject obj in baseBeatmap.HitObjects) - { - switch (obj) - { - case IHasPath: - countSlider++; - break; - - case IHasDuration: - countSpinner++; - break; - - default: - countNormal++; - break; - } - } - - int objectCount = countNormal + countSlider + countSpinner; - - DifficultyPeppyStars = (int)Math.Round( - (baseBeatmap.Difficulty.DrainRate - + baseBeatmap.Difficulty.OverallDifficulty - + baseBeatmap.Difficulty.CircleSize - + Math.Clamp(objectCount / baseBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); - - ScoreMultiplier = DifficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); - } - } - - public class OsuScoreV1Processor : ScoreV1Processor - { - public int TotalScore => BaseScore + ComboScore + BonusScore; - - /// - /// Amount of score that is combo-and-difficulty-multiplied, excluding mod multipliers. - /// - public int ComboScore { get; private set; } - - /// - /// Amount of score that is NOT combo-and-difficulty-multiplied. - /// - public int BaseScore { get; private set; } - - /// - /// Amount of score whose judgements would be treated as "bonus" in ScoreV2. - /// - public int BonusScore { get; private set; } - - private int combo; - - public OsuScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) - : base(baseBeatmap, playableBeatmap, mods) - { - foreach (var obj in playableBeatmap.HitObjects) - simulateHit(obj); - } - - private void simulateHit(HitObject hitObject) - { - bool increaseCombo = true; - bool addScoreComboMultiplier = false; - bool isBonus = false; - - int scoreIncrease = 0; - - switch (hitObject) - { - case SliderHeadCircle: - case SliderTailCircle: - case SliderRepeat: - scoreIncrease = 30; - break; - - case SliderTick: - scoreIncrease = 10; - break; - - case SpinnerBonusTick: - scoreIncrease = 1100; - increaseCombo = false; - isBonus = true; - break; - - case SpinnerTick: - scoreIncrease = 100; - increaseCombo = false; - isBonus = true; - break; - - case HitCircle: - scoreIncrease = 300; - addScoreComboMultiplier = true; - break; - - case Slider: - foreach (var nested in hitObject.NestedHitObjects) - simulateHit(nested); - - scoreIncrease = 300; - increaseCombo = false; - addScoreComboMultiplier = true; - break; - - case Spinner spinner: - // The spinner object applies a lenience because gameplay mechanics differ from osu-stable. - // We'll redo the calculations to match osu-stable here... - const double maximum_rotations_per_second = 477.0 / 60; - double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(PlayableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5); - double secondsDuration = spinner.Duration / 1000; - - // The total amount of half spins possible for the entire spinner. - int totalHalfSpinsPossible = (int)(secondsDuration * maximum_rotations_per_second * 2); - // The amount of half spins that are required to successfully complete the spinner (i.e. get a 300). - int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond); - // To be able to receive bonus points, the spinner must be rotated another 1.5 times. - int halfSpinsRequiredBeforeBonus = halfSpinsRequiredForCompletion + 3; - - for (int i = 0; i <= totalHalfSpinsPossible; i++) - { - if (i > halfSpinsRequiredBeforeBonus && (i - halfSpinsRequiredBeforeBonus) % 2 == 0) - simulateHit(new SpinnerBonusTick()); - else if (i > 1 && i % 2 == 0) - simulateHit(new SpinnerTick()); - } - - scoreIncrease = 300; - addScoreComboMultiplier = true; - break; - } - - if (addScoreComboMultiplier) - { - // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) - ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * ScoreMultiplier)); - } - - if (isBonus) - BonusScore += scoreIncrease; - else - BaseScore += scoreIncrease; - - if (increaseCombo) - combo++; - } - } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs new file mode 100644 index 0000000000..c82928b745 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs @@ -0,0 +1,167 @@ +// 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 osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.Difficulty +{ + internal class OsuScoreV1Processor + { + public int TotalScore => BaseScore + ComboScore + BonusScore; + + /// + /// Amount of score that is combo-and-difficulty-multiplied, excluding mod multipliers. + /// + public int ComboScore { get; private set; } + + /// + /// Amount of score that is NOT combo-and-difficulty-multiplied. + /// + public int BaseScore { get; private set; } + + /// + /// Amount of score whose judgements would be treated as "bonus" in ScoreV2. + /// + public int BonusScore { get; private set; } + + private int combo; + + private readonly double scoreMultiplier; + private readonly IBeatmap playableBeatmap; + + public OsuScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + { + this.playableBeatmap = playableBeatmap; + + int countNormal = 0; + int countSlider = 0; + int countSpinner = 0; + + foreach (HitObject obj in baseBeatmap.HitObjects) + { + switch (obj) + { + case IHasPath: + countSlider++; + break; + + case IHasDuration: + countSpinner++; + break; + + default: + countNormal++; + break; + } + } + + int objectCount = countNormal + countSlider + countSpinner; + + int difficultyPeppyStars = (int)Math.Round( + (baseBeatmap.Difficulty.DrainRate + + baseBeatmap.Difficulty.OverallDifficulty + + baseBeatmap.Difficulty.CircleSize + + Math.Clamp(objectCount / baseBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); + + scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + + foreach (var obj in playableBeatmap.HitObjects) + simulateHit(obj); + } + + private void simulateHit(HitObject hitObject) + { + bool increaseCombo = true; + bool addScoreComboMultiplier = false; + bool isBonus = false; + + int scoreIncrease = 0; + + switch (hitObject) + { + case SliderHeadCircle: + case SliderTailCircle: + case SliderRepeat: + scoreIncrease = 30; + break; + + case SliderTick: + scoreIncrease = 10; + break; + + case SpinnerBonusTick: + scoreIncrease = 1100; + increaseCombo = false; + isBonus = true; + break; + + case SpinnerTick: + scoreIncrease = 100; + increaseCombo = false; + isBonus = true; + break; + + case HitCircle: + scoreIncrease = 300; + addScoreComboMultiplier = true; + break; + + case Slider: + foreach (var nested in hitObject.NestedHitObjects) + simulateHit(nested); + + scoreIncrease = 300; + increaseCombo = false; + addScoreComboMultiplier = true; + break; + + case Spinner spinner: + // The spinner object applies a lenience because gameplay mechanics differ from osu-stable. + // We'll redo the calculations to match osu-stable here... + const double maximum_rotations_per_second = 477.0 / 60; + double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(playableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5); + double secondsDuration = spinner.Duration / 1000; + + // The total amount of half spins possible for the entire spinner. + int totalHalfSpinsPossible = (int)(secondsDuration * maximum_rotations_per_second * 2); + // The amount of half spins that are required to successfully complete the spinner (i.e. get a 300). + int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond); + // To be able to receive bonus points, the spinner must be rotated another 1.5 times. + int halfSpinsRequiredBeforeBonus = halfSpinsRequiredForCompletion + 3; + + for (int i = 0; i <= totalHalfSpinsPossible; i++) + { + if (i > halfSpinsRequiredBeforeBonus && (i - halfSpinsRequiredBeforeBonus) % 2 == 0) + simulateHit(new SpinnerBonusTick()); + else if (i > 1 && i % 2 == 0) + simulateHit(new SpinnerTick()); + } + + scoreIncrease = 300; + addScoreComboMultiplier = true; + break; + } + + if (addScoreComboMultiplier) + { + // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) + ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier)); + } + + if (isBonus) + BonusScore += scoreIncrease; + else + BaseScore += scoreIncrease; + + if (increaseCombo) + combo++; + } + } +} From afb5a9243a2b47e084ff1dc01f4ef2037ded3ea8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Jun 2023 05:13:22 +0900 Subject: [PATCH 0159/2100] Fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Database/RealmAccess.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 1194e1d9f8..663833575a 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -951,7 +951,7 @@ namespace osu.Game.Database { // Recalculate the old-style standardised score to see if this was an old lazer score. bool oldScoreMatchesExpectations = StandardisedScoreMigrationTools.GetOldStandardised(score) == score.TotalScore; - // Some older score don't have correct statistics populated, so let's give them benefit of doubt. + // Some older scores don't have correct statistics populated, so let's give them benefit of doubt. bool scoreIsVeryOld = score.Date < new DateTime(2023, 1, 1, 0, 0, 0); if (oldScoreMatchesExpectations || scoreIsVeryOld) From 3304e41a3012492bf054460462ea449f1d55d002 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Jun 2023 05:20:29 +0900 Subject: [PATCH 0160/2100] Add more commenting --- .../StandardisedScoreMigrationTools.cs | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index c06502b521..edc3288f61 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -27,12 +27,6 @@ namespace osu.Game.Database HitResult maxRulesetJudgement = ruleset.GetHitResults().First().result; - var maximumJudgements = score.MaximumStatistics - .Where(kvp => kvp.Key.AffectsCombo()) - .OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key)) - .SelectMany(kvp => Enumerable.Repeat(new FakeJudgement(kvp.Key), kvp.Value)) - .ToList(); - // This is a list of all results, ordered from best to worst. // We are constructing a "best possible" score from the statistics provided because it's the best we can do. List sortedHits = score.Statistics @@ -41,20 +35,29 @@ namespace osu.Game.Database .SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value)) .ToList(); + // Attempt to use maximum statistics from the database. + var maximumJudgements = score.MaximumStatistics + .Where(kvp => kvp.Key.AffectsCombo()) + .OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key)) + .SelectMany(kvp => Enumerable.Repeat(new FakeJudgement(kvp.Key), kvp.Value)) + .ToList(); + + // Some older scores may not have maximum statistics populated correctly. + // In this case we need to fill them with best-known-defaults. if (maximumJudgements.Count != sortedHits.Count) { - // Older scores may not have maximum judgements populated correctly. - // In this case we need to fill them. maximumJudgements = sortedHits .Select(r => new FakeJudgement(getMaxJudgementFor(r, maxRulesetJudgement))) .ToList(); } + // This is required to get the correct maximum combo portion. foreach (var judgement in maximumJudgements) beatmap.HitObjects.Add(new FakeHit(judgement)); - processor.ApplyBeatmap(beatmap); + // Insert all misses into a queue. + // These will be nibbled at whenever we need to reset the combo. Queue misses = new Queue(score.Statistics .Where(kvp => kvp.Key == HitResult.Miss || kvp.Key == HitResult.LargeTickMiss) .SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value))); @@ -63,7 +66,7 @@ namespace osu.Game.Database foreach (var result in sortedHits) { - // misses are handled from the queue. + // For the main part of this loop, ignore all misses, as they will be inserted from the queue. if (result == HitResult.Miss || result == HitResult.LargeTickMiss) continue; @@ -78,7 +81,8 @@ namespace osu.Game.Database } else { - // worst case scenario, insert a miss. + // We ran out of misses. But we can't let max combo increase beyond the known value, + // so let's forge a miss. processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(getMaxJudgementFor(HitResult.Miss, maxRulesetJudgement))) { Type = HitResult.Miss, @@ -169,9 +173,9 @@ namespace osu.Game.Database { public override HitResult MaxResult { get; } - public FakeJudgement(HitResult result) + public FakeJudgement(HitResult maxResult) { - MaxResult = result; + MaxResult = maxResult; } } } From c1b0c60e79c9b828b6ed69e8e34ec82f5e6cda58 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Jun 2023 05:24:04 +0900 Subject: [PATCH 0161/2100] Ensure all misses are dequeued --- .../StandardisedScoreMigrationTools.cs | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index edc3288f61..d980f7a858 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -15,6 +15,8 @@ namespace osu.Game.Database { public static long GetNewStandardised(ScoreInfo score) { + int maxJudgementIndex = 0; + // Avoid retrieving from realm inside loops. int maxCombo = score.MaxCombo; @@ -62,33 +64,15 @@ namespace osu.Game.Database .Where(kvp => kvp.Key == HitResult.Miss || kvp.Key == HitResult.LargeTickMiss) .SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value))); - int maxJudgementIndex = 0; - foreach (var result in sortedHits) { // For the main part of this loop, ignore all misses, as they will be inserted from the queue. if (result == HitResult.Miss || result == HitResult.LargeTickMiss) continue; + // Reset combo if required. if (processor.Combo.Value == maxCombo) - { - if (misses.Count > 0) - { - processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++]) - { - Type = misses.Dequeue(), - }); - } - else - { - // We ran out of misses. But we can't let max combo increase beyond the known value, - // so let's forge a miss. - processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(getMaxJudgementFor(HitResult.Miss, maxRulesetJudgement))) - { - Type = HitResult.Miss, - }); - } - } + insertMiss(); processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++]) { @@ -96,6 +80,10 @@ namespace osu.Game.Database }); } + // Ensure we haven't forgotten any misses. + while (misses.Count > 0) + insertMiss(); + var bonusHits = score.Statistics .Where(kvp => kvp.Key.IsBonus()) .SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value)); @@ -107,6 +95,26 @@ namespace osu.Game.Database // Debug.Assert(processor.HighestCombo.Value == score.MaxCombo); return processor.TotalScore.Value; + + void insertMiss() + { + if (misses.Count > 0) + { + processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++]) + { + Type = misses.Dequeue(), + }); + } + else + { + // We ran out of misses. But we can't let max combo increase beyond the known value, + // so let's forge a miss. + processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(getMaxJudgementFor(HitResult.Miss, maxRulesetJudgement))) + { + Type = HitResult.Miss, + }); + } + } } private static HitResult getMaxJudgementFor(HitResult hitResult, HitResult max) From 422e87f0ec362506ccbe16943f3732a14c9018cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Jun 2023 05:30:12 +0900 Subject: [PATCH 0162/2100] Fix weird usings --- osu.Game/Database/RealmAccess.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 663833575a..68a4679656 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -28,16 +28,12 @@ using osu.Game.IO.Legacy; using osu.Game.Models; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using osu.Game.Skinning; using Realms; using Realms.Exceptions; -using Stopwatch = System.Diagnostics.Stopwatch; namespace osu.Game.Database { From 0ab9a48f004be0e6b044391eed4b2677bf07180d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Jun 2023 11:44:52 +0900 Subject: [PATCH 0163/2100] Fix score not updating when `TrackHitEvents` is set to `false` --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index f29e3533a0..7d2bc17bda 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -33,6 +33,9 @@ namespace osu.Game.Rulesets.Scoring /// /// Whether should be populated during application of results. /// + /// + /// Should only be disabled for special cases. + /// When disabled, cannot be used. internal bool TrackHitEvents = true; /// @@ -231,10 +234,13 @@ namespace osu.Game.Rulesets.Scoring ApplyScoreChange(result); - if (!IsSimulating && TrackHitEvents) + if (!IsSimulating) { - hitEvents.Add(CreateHitEvent(result)); - lastHitObject = result.HitObject; + if (TrackHitEvents) + { + hitEvents.Add(CreateHitEvent(result)); + lastHitObject = result.HitObject; + } updateScore(); } @@ -250,6 +256,9 @@ namespace osu.Game.Rulesets.Scoring protected sealed override void RevertResultInternal(JudgementResult result) { + if (!TrackHitEvents) + throw new InvalidOperationException(@$"Rewind is not supported when {nameof(TrackHitEvents)} is disabled."); + Combo.Value = result.ComboAtJudgement; HighestCombo.Value = result.HighestComboAtJudgement; From 4cdd4561c4e00c5253ff10d735046bd0be21f715 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Jun 2023 14:17:32 +0900 Subject: [PATCH 0164/2100] Add a few more search keywords for offset settings https://github.com/ppy/osu/discussions/23898#discussioncomment-6159206 --- osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs index 1755c12f94..fc354027c1 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio { protected override LocalisableString Header => AudioSettingsStrings.OffsetHeader; - public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "universal", "uo", "timing" }); + public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "universal", "uo", "timing", "delay", "latency" }); [BackgroundDependencyLoader] private void load(OsuConfigManager config) From a5c3c9a93f4adf8ee0ceab485eb650de8e1acf70 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 12 Jun 2023 23:30:08 -0700 Subject: [PATCH 0165/2100] Use `FillFlowContainer` instead of `GridContainer` for drawable room background gradient --- .../Lounge/Components/DrawableRoom.cs | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 8c85a8235c..522438227a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -103,29 +103,25 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components CornerRadius = CORNER_RADIUS, Children = new Drawable[] { - new GridContainer + new FillFlowContainer { RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] + Direction = FillDirection.Horizontal, + Children = new Drawable[] { - new Dimension(GridSizeMode.Relative, 0.2f) - }, - Content = new[] - { - new Drawable[] + new Box { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colours.Background5, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f)) - }, - } - } + RelativeSizeAxes = Axes.Both, + Colour = colours.Background5, + Width = 0.2f, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f)), + Width = 0.8f, + }, + }, }, new Container { From 432b5e2d258091d27f3b8262fddfa35aa6b83f50 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Tue, 13 Jun 2023 15:47:44 +0900 Subject: [PATCH 0166/2100] move Inverted and color logic to ChatLine --- .../TestSceneTournamentMatchChatDisplay.cs | 4 +- osu.Game/Overlays/Chat/ChatLine.cs | 80 ++++++++++++++----- .../Overlays/Chat/DrawableChatUsername.cs | 66 ++++----------- 3 files changed, 76 insertions(+), 74 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs index fada340cf7..71d8cbeea7 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs @@ -90,7 +90,7 @@ namespace osu.Game.Tournament.Tests.Components })); AddUntilStep("message from team red is red color", () => - this.ChildrenOfType().Any(s => s.AccentColour == TournamentGame.COLOUR_RED)); + this.ChildrenOfType().Any(s => s.AccentColour.Value == TournamentGame.COLOUR_RED)); AddStep("message from team red", () => testChannel.AddNewMessages(new Message(nextMessageId()) { @@ -105,7 +105,7 @@ namespace osu.Game.Tournament.Tests.Components })); AddUntilStep("message from team blue is blue color", () => - this.ChildrenOfType().Any(s => s.AccentColour == TournamentGame.COLOUR_BLUE)); + this.ChildrenOfType().Any(s => s.AccentColour.Value == TournamentGame.COLOUR_BLUE)); AddStep("message from admin", () => testChannel.AddNewMessages(new Message(nextMessageId()) { diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 3e484d962b..2931685239 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -21,6 +21,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osuTK.Graphics; +using Message = osu.Game.Online.Chat.Message; namespace osu.Game.Overlays.Chat { @@ -69,9 +70,9 @@ namespace osu.Game.Overlays.Chat private Container? highlight; /// - /// if set, it will override or . + /// if set, it will override or . /// - public Color4? UsernameColour { get; init; } + public Color4 UsernameColour { get; init; } public ChatLine(Message message) { @@ -79,6 +80,11 @@ namespace osu.Game.Overlays.Chat RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + + // If we have custom value, this value will be override. + UsernameColour = !string.IsNullOrEmpty(message.Sender.Colour) + ? Color4Extensions.FromHex(message.Sender.Colour) + : default_colours[message.SenderId % default_colours.Length]; } [BackgroundDependencyLoader] @@ -87,27 +93,18 @@ namespace osu.Game.Overlays.Chat configManager.BindWith(OsuSetting.Prefer24HourTime, prefer24HourTime); prefer24HourTime.BindValueChanged(_ => updateTimestamp()); - if (UsernameColour != null) + drawableUsername = new DrawableChatUsername(message.Sender) { - drawableUsername = new DrawableChatUsername(message.Sender) - { - AccentColour = UsernameColour.Value - }; - } - else - { - drawableUsername = new DrawableChatUsername(message.Sender); - } + Width = UsernameWidth, + FontSize = FontSize, + AutoSizeAxes = Axes.Y, + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + Margin = new MarginPadding { Horizontal = Spacing }, + Inverted = !string.IsNullOrEmpty(message.Sender.Colour), + }; - drawableUsername.With(u => - { - u.Width = UsernameWidth; - u.FontSize = FontSize; - u.AutoSizeAxes = Axes.Y; - u.Origin = Anchor.TopRight; - u.Anchor = Anchor.TopRight; - u.Margin = new MarginPadding { Horizontal = Spacing }; - }); + drawableUsername.AccentColour.Value = UsernameColour; InternalChild = new GridContainer { @@ -178,7 +175,7 @@ namespace osu.Game.Overlays.Chat CornerRadius = 2f, Masking = true, RelativeSizeAxes = Axes.Both, - Colour = drawableUsername.AccentColour.Darken(1f), + Colour = drawableUsername.AccentColour.Value.Darken(1f), Depth = float.MaxValue, Child = new Box { RelativeSizeAxes = Axes.Both } }); @@ -216,5 +213,44 @@ namespace osu.Game.Overlays.Chat { drawableTimestamp.Text = message.Timestamp.LocalDateTime.ToLocalisableString(prefer24HourTime.Value ? @"HH:mm:ss" : @"hh:mm:ss tt"); } + + private static readonly Color4[] default_colours = + { + Color4Extensions.FromHex("588c7e"), + Color4Extensions.FromHex("b2a367"), + Color4Extensions.FromHex("c98f65"), + Color4Extensions.FromHex("bc5151"), + Color4Extensions.FromHex("5c8bd6"), + Color4Extensions.FromHex("7f6ab7"), + Color4Extensions.FromHex("a368ad"), + Color4Extensions.FromHex("aa6880"), + + Color4Extensions.FromHex("6fad9b"), + Color4Extensions.FromHex("f2e394"), + Color4Extensions.FromHex("f2ae72"), + Color4Extensions.FromHex("f98f8a"), + Color4Extensions.FromHex("7daef4"), + Color4Extensions.FromHex("a691f2"), + Color4Extensions.FromHex("c894d3"), + Color4Extensions.FromHex("d895b0"), + + Color4Extensions.FromHex("53c4a1"), + Color4Extensions.FromHex("eace5c"), + Color4Extensions.FromHex("ea8c47"), + Color4Extensions.FromHex("fc4f4f"), + Color4Extensions.FromHex("3d94ea"), + Color4Extensions.FromHex("7760ea"), + Color4Extensions.FromHex("af52c6"), + Color4Extensions.FromHex("e25696"), + + Color4Extensions.FromHex("677c66"), + Color4Extensions.FromHex("9b8732"), + Color4Extensions.FromHex("8c5129"), + Color4Extensions.FromHex("8c3030"), + Color4Extensions.FromHex("1f5d91"), + Color4Extensions.FromHex("4335a5"), + Color4Extensions.FromHex("812a96"), + Color4Extensions.FromHex("992861"), + }; } } diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index 9b802a8070..05772051da 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -33,7 +33,9 @@ namespace osu.Game.Overlays.Chat { public Action? ReportRequested; - public Color4 AccentColour { get; init; } + public Bindable AccentColour { get; } = new Bindable(); + + public bool Inverted { get; init; } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => colouredDrawable.ReceivePositionalInputAt(screenSpacePos); @@ -75,7 +77,7 @@ namespace osu.Game.Overlays.Chat private readonly APIUser user; private readonly OsuSpriteText drawableText; - private readonly Drawable colouredDrawable; + private Drawable colouredDrawable = null!; public DrawableChatUsername(APIUser user) { @@ -91,17 +93,17 @@ namespace osu.Game.Overlays.Chat Anchor = Anchor.TopRight, Origin = Anchor.TopRight, }; + } - if (string.IsNullOrWhiteSpace(user.Colour)) + [BackgroundDependencyLoader] + private void load() + { + if (!Inverted) { - AccentColour = default_colours[user.Id % default_colours.Length]; - Add(colouredDrawable = drawableText); } else { - AccentColour = Color4Extensions.FromHex(user.Colour); - Add(new Container { Anchor = Anchor.TopRight, @@ -143,9 +145,12 @@ namespace osu.Game.Overlays.Chat protected override void LoadComplete() { base.LoadComplete(); - drawableText.Colour = colours.ChatBlue; - colouredDrawable.Colour = AccentColour; + + AccentColour.BindValueChanged(c => + { + colouredDrawable.Colour = c.NewValue; + }, true); } public MenuItem[] ContextMenuItems @@ -191,7 +196,7 @@ namespace osu.Game.Overlays.Chat protected override bool OnHover(HoverEvent e) { - colouredDrawable.FadeColour(AccentColour.Lighten(0.6f), 30, Easing.OutQuint); + colouredDrawable.FadeColour(AccentColour.Value.Lighten(0.6f), 30, Easing.OutQuint); return base.OnHover(e); } @@ -200,46 +205,7 @@ namespace osu.Game.Overlays.Chat { base.OnHoverLost(e); - colouredDrawable.FadeColour(AccentColour, 800, Easing.OutQuint); + colouredDrawable.FadeColour(AccentColour.Value, 800, Easing.OutQuint); } - - private static readonly Color4[] default_colours = - { - Color4Extensions.FromHex("588c7e"), - Color4Extensions.FromHex("b2a367"), - Color4Extensions.FromHex("c98f65"), - Color4Extensions.FromHex("bc5151"), - Color4Extensions.FromHex("5c8bd6"), - Color4Extensions.FromHex("7f6ab7"), - Color4Extensions.FromHex("a368ad"), - Color4Extensions.FromHex("aa6880"), - - Color4Extensions.FromHex("6fad9b"), - Color4Extensions.FromHex("f2e394"), - Color4Extensions.FromHex("f2ae72"), - Color4Extensions.FromHex("f98f8a"), - Color4Extensions.FromHex("7daef4"), - Color4Extensions.FromHex("a691f2"), - Color4Extensions.FromHex("c894d3"), - Color4Extensions.FromHex("d895b0"), - - Color4Extensions.FromHex("53c4a1"), - Color4Extensions.FromHex("eace5c"), - Color4Extensions.FromHex("ea8c47"), - Color4Extensions.FromHex("fc4f4f"), - Color4Extensions.FromHex("3d94ea"), - Color4Extensions.FromHex("7760ea"), - Color4Extensions.FromHex("af52c6"), - Color4Extensions.FromHex("e25696"), - - Color4Extensions.FromHex("677c66"), - Color4Extensions.FromHex("9b8732"), - Color4Extensions.FromHex("8c5129"), - Color4Extensions.FromHex("8c3030"), - Color4Extensions.FromHex("1f5d91"), - Color4Extensions.FromHex("4335a5"), - Color4Extensions.FromHex("812a96"), - Color4Extensions.FromHex("992861"), - }; } } From 0adb399ea869c758796790edb8184df173c43f69 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 13 Jun 2023 00:09:58 -0700 Subject: [PATCH 0167/2100] Use anchor instead of `FillFlowContainer` --- .../Lounge/Components/DrawableRoom.cs | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 522438227a..f1fc751630 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -103,25 +103,19 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components CornerRadius = CORNER_RADIUS, Children = new Drawable[] { - new FillFlowContainer + new Box { RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colours.Background5, - Width = 0.2f, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f)), - Width = 0.8f, - }, - }, + Colour = colours.Background5, + Width = 0.2f, + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f)), + Width = 0.8f, }, new Container { From aa644832dccf4a979b67f53a605cb644f02fbaa8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 13 Jun 2023 02:33:22 +0900 Subject: [PATCH 0168/2100] Add ScoreV1 calculation for TaikoRuleset --- .../TestSceneFlyingHits.cs | 2 +- .../Difficulty/TaikoDifficultyCalculator.cs | 8 + .../Difficulty/TaikoScoreV1Processor.cs | 196 ++++++++++++++++++ osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 2 +- .../Objects/DrumRollTick.cs | 7 + 5 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs index e0ff617b59..88af50d36b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Tests private void addFlyingHit(HitType hitType) { - var tick = new DrumRollTick { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current }; + var tick = new DrumRollTick(null) { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current }; DrawableDrumRollTick h; DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType }); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 24b5f5939a..28b07c0d59 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -27,9 +27,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public override int Version => 20220902; + private readonly IWorkingBeatmap workingBeatmap; + public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { + workingBeatmap = beatmap; } protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) @@ -86,6 +89,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty HitWindows hitWindows = new TaikoHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + TaikoScoreV1Processor sv1Processor = new TaikoScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); + return new TaikoDifficultyAttributes { StarRating = starRating, @@ -96,6 +101,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty PeakDifficulty = combinedRating, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), + LegacyTotalScore = sv1Processor.TotalScore, + LegacyComboScore = sv1Processor.ComboScore, + LegacyBonusScore = sv1Processor.BonusScore }; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs new file mode 100644 index 0000000000..ee52424b26 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs @@ -0,0 +1,196 @@ +// 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 osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Difficulty +{ + internal class TaikoScoreV1Processor + { + public int TotalScore => BaseScore + ComboScore + BonusScore; + + /// + /// Amount of score that is combo-and-difficulty-multiplied, excluding mod multipliers. + /// + public int ComboScore { get; private set; } + + /// + /// Amount of score that is NOT combo-and-difficulty-multiplied. + /// + public int BaseScore { get; private set; } + + /// + /// Amount of score whose judgements would be treated as "bonus" in ScoreV2. + /// + public int BonusScore { get; private set; } + + private int combo; + + private readonly double modMultiplier; + private readonly int difficultyPeppyStars; + private readonly IBeatmap playableBeatmap; + private readonly IReadOnlyList mods; + + public TaikoScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + { + this.playableBeatmap = playableBeatmap; + this.mods = mods; + + int countNormal = 0; + int countSlider = 0; + int countSpinner = 0; + + foreach (HitObject obj in baseBeatmap.HitObjects) + { + switch (obj) + { + case IHasPath: + countSlider++; + break; + + case IHasDuration: + countSpinner++; + break; + + default: + countNormal++; + break; + } + } + + int objectCount = countNormal + countSlider + countSpinner; + + difficultyPeppyStars = (int)Math.Round( + (baseBeatmap.Difficulty.DrainRate + + baseBeatmap.Difficulty.OverallDifficulty + + baseBeatmap.Difficulty.CircleSize + + Math.Clamp(objectCount / baseBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); + + modMultiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + + foreach (var obj in playableBeatmap.HitObjects) + simulateHit(obj); + } + + private void simulateHit(HitObject hitObject) + { + bool increaseCombo = true; + bool addScoreComboMultiplier = false; + bool isBonus = false; + + int scoreIncrease = 0; + + switch (hitObject) + { + case SwellTick: + scoreIncrease = 300; + increaseCombo = false; + break; + + case DrumRollTick: + scoreIncrease = 300; + increaseCombo = false; + isBonus = true; + break; + + case Swell swell: + // The taiko swell generally does not match the osu-stable implementation in any way. + // We'll redo the calculations to match osu-stable here... + double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(playableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5); + double secondsDuration = swell.Duration / 1000; + + // The amount of half spins that are required to successfully complete the spinner (i.e. get a 300). + int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond); + + halfSpinsRequiredForCompletion = (int)Math.Max(1, halfSpinsRequiredForCompletion * 1.65f); + + if (mods.Any(m => m is ModDoubleTime)) + halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 0.75f)); + if (mods.Any(m => m is ModHalfTime)) + halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 1.5f)); + + for (int i = 0; i <= halfSpinsRequiredForCompletion; i++) + simulateHit(new SwellTick()); + + scoreIncrease = 300; + addScoreComboMultiplier = true; + increaseCombo = false; + isBonus = true; + break; + + case Hit: + scoreIncrease = 300; + addScoreComboMultiplier = true; + break; + + case DrumRoll: + foreach (var nested in hitObject.NestedHitObjects) + simulateHit(nested); + return; + } + + if (hitObject is DrumRollTick tick) + { + if (playableBeatmap.ControlPointInfo.EffectPointAt(tick.Parent.StartTime).KiaiMode) + scoreIncrease = (int)(scoreIncrease * 1.2f); + + if (tick.IsStrong) + scoreIncrease += scoreIncrease / 5; + } + + // The score increase directly contributed to by the combo-multiplied portion. + int comboScoreIncrease = 0; + + if (addScoreComboMultiplier) + { + int oldScoreIncrease = scoreIncrease; + + // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) + scoreIncrease += (int)(scoreIncrease / 35 * 2 * (difficultyPeppyStars + 1) * modMultiplier) * (Math.Min(100, combo) / 10); + + if (hitObject is Swell) + { + if (playableBeatmap.ControlPointInfo.EffectPointAt(hitObject.GetEndTime()).KiaiMode) + scoreIncrease = (int)(scoreIncrease * 1.2f); + } + else + { + if (playableBeatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode) + scoreIncrease = (int)(scoreIncrease * 1.2f); + } + + comboScoreIncrease = scoreIncrease - oldScoreIncrease; + } + + if (hitObject is Swell || (hitObject is TaikoStrongableHitObject strongable && strongable.IsStrong)) + { + scoreIncrease *= 2; + comboScoreIncrease *= 2; + } + + scoreIncrease -= comboScoreIncrease; + + if (addScoreComboMultiplier) + ComboScore += comboScoreIncrease; + + if (isBonus) + BonusScore += scoreIncrease; + else + BaseScore += scoreIncrease; + + if (increaseCombo) + combo++; + + if (hitObject is Swell) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 2f4a98bd8f..76d1a58506 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Objects { cancellationToken.ThrowIfCancellationRequested(); - AddNested(new DrumRollTick + AddNested(new DrumRollTick(this) { FirstTick = first, TickSpacing = tickSpacing, diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs index 206e8ecb5a..a8f309f7a6 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Taiko.Objects { public class DrumRollTick : TaikoStrongableHitObject { + public readonly DrumRoll Parent; + /// /// Whether this is the first (initial) tick of the slider. /// @@ -27,6 +29,11 @@ namespace osu.Game.Rulesets.Taiko.Objects /// public double HitWindow => TickSpacing / 2; + public DrumRollTick(DrumRoll parent) + { + Parent = parent; + } + public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; From 3ec97121e1a62bbfdec75de21d0afa6d1b94098b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 13 Jun 2023 19:41:39 +0900 Subject: [PATCH 0169/2100] Add ScoreV1 calculation for CatchRuleset --- .../Difficulty/CatchDifficultyCalculator.cs | 8 ++ .../Difficulty/CatchScoreV1Processor.cs | 133 ++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 42cfde268e..fb7c4f05f4 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -25,9 +25,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty public override int Version => 20220701; + private readonly IWorkingBeatmap workingBeatmap; + public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { + workingBeatmap = beatmap; } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) @@ -38,12 +41,17 @@ namespace osu.Game.Rulesets.Catch.Difficulty // this is the same as osu!, so there's potential to share the implementation... maybe double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; + CatchScoreV1Processor sv1Processor = new CatchScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); + return new CatchDifficultyAttributes { StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor, Mods = mods, ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)), + LegacyTotalScore = sv1Processor.TotalScore, + LegacyComboScore = sv1Processor.ComboScore, + LegacyBonusScore = sv1Processor.BonusScore }; } diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs new file mode 100644 index 0000000000..b5c3838fdc --- /dev/null +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs @@ -0,0 +1,133 @@ +// 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 osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; + +namespace osu.Game.Rulesets.Catch.Difficulty +{ + internal class CatchScoreV1Processor + { + public int TotalScore => BaseScore + ComboScore + BonusScore; + + /// + /// Amount of score that is combo-and-difficulty-multiplied, excluding mod multipliers. + /// + public int ComboScore { get; private set; } + + /// + /// Amount of score that is NOT combo-and-difficulty-multiplied. + /// + public int BaseScore { get; private set; } + + /// + /// Amount of score whose judgements would be treated as "bonus" in ScoreV2. + /// + public int BonusScore { get; private set; } + + private int combo; + + private readonly double scoreMultiplier; + + public CatchScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + { + int countNormal = 0; + int countSlider = 0; + int countSpinner = 0; + + foreach (HitObject obj in baseBeatmap.HitObjects) + { + switch (obj) + { + case IHasPath: + countSlider++; + break; + + case IHasDuration: + countSpinner++; + break; + + default: + countNormal++; + break; + } + } + + int objectCount = countNormal + countSlider + countSpinner; + + int difficultyPeppyStars = (int)Math.Round( + (baseBeatmap.Difficulty.DrainRate + + baseBeatmap.Difficulty.OverallDifficulty + + baseBeatmap.Difficulty.CircleSize + + Math.Clamp(objectCount / baseBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); + + scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + + foreach (var obj in playableBeatmap.HitObjects) + simulateHit(obj); + } + + private void simulateHit(HitObject hitObject) + { + bool increaseCombo = true; + bool addScoreComboMultiplier = false; + bool isBonus = false; + + int scoreIncrease = 0; + + switch (hitObject) + { + case TinyDroplet: + scoreIncrease = 10; + increaseCombo = false; + break; + + case Droplet: + scoreIncrease = 100; + break; + + case Fruit: + scoreIncrease = 300; + addScoreComboMultiplier = true; + increaseCombo = true; + break; + + case Banana: + scoreIncrease = 1100; + increaseCombo = false; + isBonus = true; + break; + + case JuiceStream: + foreach (var nested in hitObject.NestedHitObjects) + simulateHit(nested); + return; + + case BananaShower: + foreach (var nested in hitObject.NestedHitObjects) + simulateHit(nested); + return; + } + + if (addScoreComboMultiplier) + { + // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) + ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier)); + } + + if (isBonus) + BonusScore += scoreIncrease; + else + BaseScore += scoreIncrease; + + if (increaseCombo) + combo++; + } + } +} From 13d1f9c902c68c708cde89cd162de8f0c9f3a1ed Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 13 Jun 2023 23:22:27 +0900 Subject: [PATCH 0170/2100] Add ScoreV1 calculation for ManiaRuleset --- .../Difficulty/ManiaDifficultyCalculator.cs | 10 +++++++- .../Difficulty/ManiaScoreV1Processor.cs | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 63e61f17e3..cb41b93deb 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -33,9 +33,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty public override int Version => 20220902; + private readonly IWorkingBeatmap workingBeatmap; + public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { + workingBeatmap = beatmap; + isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset); originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty; } @@ -48,6 +52,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty HitWindows hitWindows = new ManiaHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + ManiaScoreV1Processor sv1Processor = new ManiaScoreV1Processor(mods); + return new ManiaDifficultyAttributes { StarRating = skills[0].DifficultyValue() * star_scaling_factor, @@ -55,7 +61,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty // In osu-stable mania, rate-adjustment mods don't affect the hit window. // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being. GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate), - MaxCombo = beatmap.HitObjects.Sum(maxComboForObject) + MaxCombo = beatmap.HitObjects.Sum(maxComboForObject), + LegacyTotalScore = sv1Processor.TotalScore, + LegacyComboScore = sv1Processor.TotalScore }; } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs new file mode 100644 index 0000000000..5712205e8f --- /dev/null +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs @@ -0,0 +1,24 @@ +// 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 osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Mania.Difficulty +{ + internal class ManiaScoreV1Processor + { + public int TotalScore { get; private set; } + + public ManiaScoreV1Processor(IReadOnlyList mods) + { + double multiplier = mods.Where(m => m is not (ModHidden or ModHardRock or ModDoubleTime or ModFlashlight or ManiaModFadeIn)) + .Select(m => m.ScoreMultiplier) + .Aggregate((c, n) => c * n); + + TotalScore = (int)(1000000 * multiplier); + } + } +} From 51451bd53e92c4e160bfee4d319669cc9937b7e8 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Wed, 14 Jun 2023 01:32:27 +0900 Subject: [PATCH 0171/2100] fix test --- .../Visual/Online/TestSceneChatLineTruncation.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs index d816555237..d1c380e2c7 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs @@ -41,10 +41,14 @@ namespace osu.Game.Tests.Visual.Online private void addMessageWithChecks(string text, bool isAction = false, bool isImportant = false, string username = null, Colour4? color = null) { int index = textContainer.Count + 1; - var newLine = new ChatLine(new DummyMessage(text, isAction, isImportant, index, username)) - { - UsernameColour = color - }; + + var newLine = color != null + ? new ChatLine(new DummyMessage(text, isAction, isImportant, index, username)) + { + UsernameColour = color.Value + } + : new ChatLine(new DummyMessage(text, isAction, isImportant, index, username)); + textContainer.Add(newLine); } From 3334323eb7cdecf753fe3266e5edc85e332b6373 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Jun 2023 01:54:57 +0900 Subject: [PATCH 0172/2100] Run `updateScore` on `Reset` for good measure --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 7d2bc17bda..35a7dfe369 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -328,6 +328,9 @@ namespace osu.Game.Rulesets.Scoring /// Whether to store the current state of the for future use. protected override void Reset(bool storeResults) { + // Run one last time to store max values. + updateScore(); + base.Reset(storeResults); hitEvents.Clear(); From 6205864c62571faea9a6af432091c3f7b30a4fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 13 Jun 2023 18:45:35 +0200 Subject: [PATCH 0173/2100] Fix score migration not considering mod multipliers --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index d980f7a858..af91bee9e4 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -57,6 +57,7 @@ namespace osu.Game.Database foreach (var judgement in maximumJudgements) beatmap.HitObjects.Add(new FakeHit(judgement)); processor.ApplyBeatmap(beatmap); + processor.Mods.Value = score.Mods; // Insert all misses into a queue. // These will be nibbled at whenever we need to reset the combo. @@ -162,7 +163,12 @@ namespace osu.Game.Database break; } - return (long)(1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore); + double modMultiplier = 1; + + foreach (var mod in score.Mods) + modMultiplier *= mod.ScoreMultiplier; + + return (long)((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier); } private class FakeHit : HitObject From f553efba8a03639f8e086a244eabfbc8b2527759 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Jun 2023 02:29:56 +0900 Subject: [PATCH 0174/2100] Fix playback controls in editor handling key repeat when they probably shouldn't Closes https://github.com/ppy/osu/issues/23903. --- osu.Game/Screens/Edit/Components/PlaybackControl.cs | 3 +++ osu.Game/Screens/Edit/Editor.cs | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index 72c299f443..431336aa60 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -76,6 +76,9 @@ namespace osu.Game.Screens.Edit.Components protected override bool OnKeyDown(KeyDownEvent e) { + if (e.Repeat) + return false; + switch (e.Key) { case Key.Space: diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index bb052b1d22..74947aab09 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -533,6 +533,9 @@ namespace osu.Game.Screens.Edit // Track traversal keys. // Matching osu-stable implementations. case Key.Z: + if (e.Repeat) + return false; + // Seek to first object time, or track start if already there. double? firstObjectTime = editorBeatmap.HitObjects.FirstOrDefault()?.StartTime; @@ -543,12 +546,18 @@ namespace osu.Game.Screens.Edit return true; case Key.X: + if (e.Repeat) + return false; + // Restart playback from beginning of track. clock.Seek(0); clock.Start(); return true; case Key.C: + if (e.Repeat) + return false; + // Pause or resume. if (clock.IsRunning) clock.Stop(); @@ -557,6 +566,9 @@ namespace osu.Game.Screens.Edit return true; case Key.V: + if (e.Repeat) + return false; + // Seek to last object time, or track end if already there. // Note that in osu-stable subsequent presses when at track end won't return to last object. // This has intentionally been changed to make it more useful. From df49a48d4d70e3a365cccd76778c0b8f60f0adb2 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 13 Jun 2023 13:48:47 -0700 Subject: [PATCH 0175/2100] Put left and right details inside `GridContainer` --- .../Lounge/Components/DrawableRoom.cs | 163 ++++++++++-------- 1 file changed, 89 insertions(+), 74 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index f1fc751630..f26d9b168d 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -117,94 +117,109 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f)), Width = 0.8f, }, - new Container + new GridContainer { - Name = @"Left details", RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding + ColumnDimensions = new[] { - Left = 20, - Vertical = 5 + new Dimension(), + new Dimension(GridSizeMode.AutoSize), }, - Children = new Drawable[] + Content = new[] { - new FillFlowContainer + new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] + new Container { - new FillFlowContainer + Name = @"Left details", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - Children = new Drawable[] - { - new RoomStatusPill - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft - }, - specialCategoryPill = new RoomSpecialCategoryPill - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft - }, - endDateInfo = new EndDateInfo - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - } + Left = 20, + Vertical = 5 }, - new FillFlowContainer + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Top = 3 }, - Direction = FillDirection.Vertical, - Children = new Drawable[] + new FillFlowContainer { - new RoomNameText(), - new RoomStatusText() + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new RoomStatusPill + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + specialCategoryPill = new RoomSpecialCategoryPill + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + endDateInfo = new EndDateInfo + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + } + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Top = 3 }, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new RoomNameText(), + new RoomStatusText() + } + } + }, + }, + new FillFlowContainer + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + ChildrenEnumerable = CreateBottomDetails() + } + } + }, + new FillFlowContainer + { + Name = "Right content", + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Spacing = new Vector2(5), + Padding = new MarginPadding + { + Right = 10, + Vertical = 20, + }, + Children = new Drawable[] + { + ButtonsContainer, + drawableRoomParticipantsList = new DrawableRoomParticipantsList + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + NumberOfCircles = NumberOfAvatars } } }, - }, - new FillFlowContainer - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - ChildrenEnumerable = CreateBottomDetails() - } - } - }, - new FillFlowContainer - { - Name = "Right content", - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Spacing = new Vector2(5), - Padding = new MarginPadding - { - Right = 10, - Vertical = 20, - }, - Children = new Drawable[] - { - ButtonsContainer, - drawableRoomParticipantsList = new DrawableRoomParticipantsList - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - NumberOfCircles = NumberOfAvatars } } }, From 2a3f2ff122e0ec8fe0d26ddc9a6b72c4ca253af9 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 13 Jun 2023 13:56:29 -0700 Subject: [PATCH 0176/2100] Truncate room name text --- .../Lounge/Components/DrawableRoom.cs | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index f26d9b168d..11c649a9ae 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -179,7 +179,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Direction = FillDirection.Vertical, Children = new Drawable[] { - new RoomNameText(), + new TruncatingSpriteText + { + RelativeSizeAxes = Axes.X, + Font = OsuFont.GetFont(size: 28), + Current = { BindTarget = Room.Name } + }, new RoomStatusText() } } @@ -316,23 +321,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components return pills; } - private partial class RoomNameText : OsuSpriteText - { - [Resolved(typeof(Room), nameof(Online.Rooms.Room.Name))] - private Bindable name { get; set; } - - public RoomNameText() - { - Font = OsuFont.GetFont(size: 28); - } - - [BackgroundDependencyLoader] - private void load() - { - Current = name; - } - } - private partial class RoomStatusText : OnlinePlayComposite { [Resolved] From 05bdc924267edf3afdea1a8573cc5ce603c5c1e5 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 13 Jun 2023 13:58:58 -0700 Subject: [PATCH 0177/2100] Add right padding according to right detail shear --- .../OnlinePlay/Lounge/Components/DrawableRoom.cs | 1 + .../Components/DrawableRoomParticipantsList.cs | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 11c649a9ae..ad421fce94 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -136,6 +136,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Padding = new MarginPadding { Left = 20, + Right = DrawableRoomParticipantsList.SHEAR_WIDTH, Vertical = 5 }, Children = new Drawable[] diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index c31633eefc..06f9f35479 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -24,8 +24,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public partial class DrawableRoomParticipantsList : OnlinePlayComposite { + public const float SHEAR_WIDTH = 12f; + private const float avatar_size = 36; + private const float height = 60f; + + private static readonly Vector2 shear = new Vector2(SHEAR_WIDTH / height, 0); + private FillFlowContainer avatarFlow; private CircularAvatar hostAvatar; @@ -36,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public DrawableRoomParticipantsList() { AutoSizeAxes = Axes.X; - Height = 60; + Height = height; } [BackgroundDependencyLoader] @@ -49,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.Both, Masking = true, CornerRadius = 10, - Shear = new Vector2(0.2f, 0), + Shear = shear, Child = new Box { RelativeSizeAxes = Axes.Both, @@ -98,7 +104,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.Both, Masking = true, CornerRadius = 10, - Shear = new Vector2(0.2f, 0), + Shear = shear, Child = new Box { RelativeSizeAxes = Axes.Both, From 83abc80950f5e19c590d8c5099aaf872411a7708 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Jun 2023 13:54:37 +0900 Subject: [PATCH 0178/2100] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index b2faa7dfc2..e08b09aef9 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From 24d8e336e24fbdb1f4fdfe01f7c2336546a3184b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 14 Jun 2023 07:55:03 +0300 Subject: [PATCH 0179/2100] Remove width limit on room status text in favour of cell distribution --- osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index ad421fce94..ef06d21655 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -337,7 +337,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Width = 0.5f; } [BackgroundDependencyLoader] From 6543c720efcf154f0d29b8e839aa076e5a8359d0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 14 Jun 2023 09:58:39 +0300 Subject: [PATCH 0180/2100] Fix map pool test scene using invalid mod acronyms --- osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs index 5695cb5574..48375c2cbd 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs @@ -92,7 +92,7 @@ namespace osu.Game.Tournament.Tests.Screens Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); for (int i = 0; i < 11; i++) - addBeatmap(i > 4 ? $"M{i}" : "NM"); + addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM"); }); AddStep("reset match", () => @@ -118,7 +118,7 @@ namespace osu.Game.Tournament.Tests.Screens Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); for (int i = 0; i < 12; i++) - addBeatmap(i > 4 ? $"M{i}" : "NM"); + addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM"); }); AddStep("reset match", () => @@ -130,7 +130,7 @@ namespace osu.Game.Tournament.Tests.Screens assertThreeWide(); } - private void addBeatmap(string mods = "nm") + private void addBeatmap(string mods = "NM") { Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Add(new RoundBeatmap { From 90a5c75474275cd56bfd8031bd35b6a94d7c3e4c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 14 Jun 2023 10:01:01 +0300 Subject: [PATCH 0181/2100] Add setting to ladder info and simplify changes --- osu.Game.Tournament/Models/LadderInfo.cs | 2 + .../Screens/MapPool/MapPoolScreen.cs | 79 +++++++++---------- 2 files changed, 39 insertions(+), 42 deletions(-) diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs index 6b64a1156e..cb4e8bc16a 100644 --- a/osu.Game.Tournament/Models/LadderInfo.cs +++ b/osu.Game.Tournament/Models/LadderInfo.cs @@ -42,5 +42,7 @@ namespace osu.Game.Tournament.Models }; public Bindable AutoProgressScreens = new BindableBool(true); + + public Bindable SplitMapPoolByMods = new BindableBool(true); } } diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index fcb0c4d70b..cb6c5902ec 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays.Settings; using osu.Game.Tournament.Components; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; @@ -25,8 +24,7 @@ namespace osu.Game.Tournament.Screens.MapPool { public partial class MapPoolScreen : TournamentMatchScreen { - private readonly FillFlowContainer> mapFlows; - private TournamentMatch currentMatch; + private FillFlowContainer> mapFlows; [Resolved(canBeNull: true)] private TournamentSceneManager sceneManager { get; set; } @@ -34,14 +32,13 @@ namespace osu.Game.Tournament.Screens.MapPool private TeamColour pickColour; private ChoiceType pickType; - private readonly OsuButton buttonRedBan; - private readonly OsuButton buttonBlueBan; - private readonly OsuButton buttonRedPick; - private readonly OsuButton buttonBluePick; + private OsuButton buttonRedBan; + private OsuButton buttonBlueBan; + private OsuButton buttonRedPick; + private OsuButton buttonBluePick; - private readonly SettingsCheckbox chkBoxLineBreak; - - public MapPoolScreen() + [BackgroundDependencyLoader] + private void load(MatchIPCInfo ipc) { InternalChildren = new Drawable[] { @@ -102,26 +99,26 @@ namespace osu.Game.Tournament.Screens.MapPool Action = reset }, new ControlPanel.Spacer(), - new TournamentSpriteText + new OsuCheckbox { - Text = "Each modpool takes" + LabelText = "Split display by mods", + Current = LadderInfo.SplitMapPoolByMods, }, - new TournamentSpriteText - { - Text = "different row" - }, - chkBoxLineBreak = new SettingsCheckbox() }, } }; + + ipc.Beatmap.BindValueChanged(beatmapChanged); } - [BackgroundDependencyLoader] - private void load(MatchIPCInfo ipc) + private Bindable splitMapPoolByMods; + + protected override void LoadComplete() { - ipc.Beatmap.BindValueChanged(beatmapChanged); - chkBoxLineBreak.Current.Value = true; - chkBoxLineBreak.Current.BindValueChanged(_ => rearrangeMappool()); + base.LoadComplete(); + + splitMapPoolByMods = LadderInfo.SplitMapPoolByMods.GetBoundCopy(); + splitMapPoolByMods.BindValueChanged(_ => updateDisplay()); } private void beatmapChanged(ValueChangedEvent beatmap) @@ -228,42 +225,40 @@ namespace osu.Game.Tournament.Screens.MapPool protected override void CurrentMatchChanged(ValueChangedEvent match) { base.CurrentMatchChanged(match); - currentMatch = match.NewValue; - rearrangeMappool(); + updateDisplay(); } - private void rearrangeMappool() + private void updateDisplay() { mapFlows.Clear(); - if (currentMatch == null) + if (CurrentMatch.Value == null) return; + int totalRows = 0; - if (currentMatch.Round.Value != null) + if (CurrentMatch.Value.Round.Value != null) { FillFlowContainer currentFlow = null; - string currentMod = null; + string currentMods = null; int flowCount = 0; - foreach (var b in currentMatch.Round.Value.Beatmaps) + foreach (var b in CurrentMatch.Value.Round.Value.Beatmaps) { - if (currentFlow == null || currentMod != b.Mods) + if (currentFlow == null || (LadderInfo.SplitMapPoolByMods.Value && currentMods != b.Mods)) { - if (chkBoxLineBreak.Current.Value || currentFlow == null) + mapFlows.Add(currentFlow = new FillFlowContainer { - mapFlows.Add(currentFlow = new FillFlowContainer - { - Spacing = new Vector2(10, 5), - Direction = FillDirection.Full, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - }); + Spacing = new Vector2(10, 5), + Direction = FillDirection.Full, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }); - totalRows++; - flowCount = 0; - } - currentMod = b.Mods; + currentMods = b.Mods; + + totalRows++; + flowCount = 0; } if (++flowCount > 2) From 78fe71182430e145a4d647b40c15a6118b448550 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 14 Jun 2023 10:01:52 +0300 Subject: [PATCH 0182/2100] Add visual test case --- .../Screens/TestSceneMapPoolScreen.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs index 48375c2cbd..0ffbeeb491 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs @@ -24,6 +24,9 @@ namespace osu.Game.Tournament.Tests.Screens Add(screen = new MapPoolScreen { Width = 0.7f }); } + [SetUp] + public void SetUp() => Schedule(() => Ladder.SplitMapPoolByMods.Value = true); + [Test] public void TestFewMaps() { @@ -130,6 +133,26 @@ namespace osu.Game.Tournament.Tests.Screens assertThreeWide(); } + [Test] + public void TestDisableMapsPerMod() + { + AddStep("load many maps", () => + { + Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); + + for (int i = 0; i < 12; i++) + addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM"); + }); + + AddStep("disable maps per mod", () => Ladder.SplitMapPoolByMods.Value = false); + + AddStep("reset match", () => + { + Ladder.CurrentMatch.Value = new TournamentMatch(); + Ladder.CurrentMatch.Value = Ladder.Matches.First(); + }); + } + private void addBeatmap(string mods = "NM") { Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Add(new RoundBeatmap From 758831b983e86a3c178a0ae1e4efeaf9bdba2032 Mon Sep 17 00:00:00 2001 From: tsrk Date: Wed, 14 Jun 2023 13:25:24 +0200 Subject: [PATCH 0183/2100] test: remove hard usages of `KeyCounterDisplay` --- osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs | 7 ++++--- .../Visual/Gameplay/TestSceneGameplayRewinding.cs | 6 ++++-- osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs | 11 +++++++---- osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs | 5 ++++- .../Gameplay/TestSceneSkinEditorMultipleSkins.cs | 6 ------ .../Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs | 6 +++--- 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index f3f942b74b..f628709db0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.Break; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; using osu.Game.Users.Drawables; @@ -35,14 +36,14 @@ namespace osu.Game.Tests.Visual.Gameplay var referenceBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); - AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Counters.Any(kc => kc.CountPresses.Value > 2)); + AddUntilStep("key counter counted keys", () => Player.HUDOverlay.ChildrenOfType().FirstOrDefault()?.Counters.Any(kc => kc.CountPresses.Value > 2) ?? false); seekTo(referenceBeatmap.Breaks[0].StartTime); - AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting.Value); + AddAssert("keys not counting", () => !Player.HUDOverlay.ChildrenOfType().FirstOrDefault()?.IsCounting.Value ?? false); AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType().Single().AccuracyDisplay.Current.Value == 1); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); - AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Counters.All(kc => kc.CountPresses.Value == 0)); + AddUntilStep("key counter reset", () => Player.HUDOverlay.ChildrenOfType().FirstOrDefault()?.Counters.All(kc => kc.CountPresses.Value == 0) ?? false); seekTo(referenceBeatmap.HitObjects[^1].GetEndTime()); AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs index 751aeb4e13..3d920a3e92 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs @@ -7,11 +7,13 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Play.HUD; using osu.Game.Storyboards; using osuTK; @@ -31,11 +33,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); addSeekStep(3000); AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged)); - AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Counters.Select(kc => kc.CountPresses.Value).Sum() == 15); + AddUntilStep("key counter counted keys", () => Player.HUDOverlay.ChildrenOfType()?.FirstOrDefault()?.Counters.Select(kc => kc.CountPresses.Value).Sum() == 15); AddStep("clear results", () => Player.Results.Clear()); addSeekStep(0); AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged)); - AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Counters.All(kc => kc.CountPresses.Value == 0)); + AddUntilStep("key counters reset", () => Player.HUDOverlay.ChildrenOfType().FirstOrDefault()?.Counters.All(kc => kc.CountPresses.Value == 0) ?? false); AddAssert("no results triggered", () => Player.Results.Count == 0); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index f97019e466..857d2b1d2d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -44,8 +45,8 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock()); // best way to check without exposing. - private Drawable hideTarget => hudOverlay.KeyCounter; - private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single(); + private Drawable hideTarget => hudOverlay.ChildrenOfType().First(); + private Drawable keyCounterFlow => hudOverlay.ChildrenOfType().First().ChildrenOfType>().Single(); [BackgroundDependencyLoader] private void load() @@ -138,7 +139,9 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("hide key overlay", () => { localConfig.SetValue(OsuSetting.KeyOverlay, false); - hudOverlay.KeyCounter.AlwaysVisible.Value = false; + var kcd = hudOverlay.ChildrenOfType().FirstOrDefault(); + if (kcd != null) + kcd.AlwaysVisible.Value = false; }); AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); @@ -267,7 +270,7 @@ namespace osu.Game.Tests.Visual.Gameplay hudOverlay = new HUDOverlay(null, Array.Empty()); // Add any key just to display the key counter visually. - hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); + hudOverlay.ChildrenOfType().ForEach(k => k.Add(new KeyCounterKeyboardTrigger(Key.Space))); scoreProcessor.Combo.Value = 1; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs index bf9b13b320..1fe7fc9063 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs @@ -6,11 +6,13 @@ using System; using System.ComponentModel; using System.Linq; +using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; namespace osu.Game.Tests.Visual.Gameplay { @@ -27,7 +29,8 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { AddUntilStep("score above zero", () => ((ScoreAccessibleReplayPlayer)Player).ScoreProcessor.TotalScore.Value > 0); - AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Counters.Any(kc => kc.CountPresses.Value > 0)); + AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.ChildrenOfType().FirstOrDefault()?.Counters.Any(kc => kc.CountPresses + .Value > 0) ?? false); AddAssert("cannot fail", () => !((ScoreAccessibleReplayPlayer)Player).AllowFail); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index 4ae115a68d..25cbe54cc3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -14,9 +14,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Edit; using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD; using osu.Game.Tests.Gameplay; -using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { @@ -57,10 +55,6 @@ namespace osu.Game.Tests.Visual.Gameplay Origin = Anchor.Centre, }; - // Add any key just to display the key counter visually. - hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); - scoreProcessor.Combo.Value = 1; - return new Container { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index 89432940ba..47f1ebf024 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -43,8 +43,8 @@ namespace osu.Game.Tests.Visual.Gameplay private IEnumerable hudOverlays => CreatedDrawables.OfType(); // best way to check without exposing. - private Drawable hideTarget => hudOverlay.KeyCounter; - private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single(); + private Drawable hideTarget => hudOverlay.ChildrenOfType().First(); + private Drawable keyCounterFlow => hudOverlay.ChildrenOfType().First().ChildrenOfType>().Single(); [Test] public void TestComboCounterIncrementing() @@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Gameplay hudOverlay = new HUDOverlay(null, Array.Empty()); // Add any key just to display the key counter visually. - hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); + hudOverlay.ChildrenOfType().ForEach(k => k.Add(new KeyCounterKeyboardTrigger(Key.Space))); action?.Invoke(hudOverlay); From ed95fb59823d4c2459f8921aaa0c883535bb6e37 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 14 Jun 2023 07:30:46 -0700 Subject: [PATCH 0184/2100] Revert blocking password requirement text localisation --- osu.Game/Localisation/AccountCreationStrings.cs | 11 ----------- osu.Game/Overlays/AccountCreation/ScreenEntry.cs | 9 +++------ 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/osu.Game/Localisation/AccountCreationStrings.cs b/osu.Game/Localisation/AccountCreationStrings.cs index 6acfaaa9ac..3884e4d8bf 100644 --- a/osu.Game/Localisation/AccountCreationStrings.cs +++ b/osu.Game/Localisation/AccountCreationStrings.cs @@ -51,17 +51,6 @@ namespace osu.Game.Localisation /// public static LocalisableString MakeSureToGetIt => new TranslatableString(getKey(@"make_sure_to_get_it"), @" Make sure to get it right!"); - /// - /// "At least {0}. Choose something long but also something you will remember, like a line from your favourite song." - /// - public static LocalisableString PasswordRequirements(string arg0) => new TranslatableString(getKey(@"password_requirements"), - @"At least {0}. Choose something long but also something you will remember, like a line from your favourite song.", arg0); - - /// - /// "8 characters long" - /// - public static LocalisableString CharactersLong => new TranslatableString(getKey(@"characters_long"), @"8 characters long"); - private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index 5725f9cf7d..ec3e7f893f 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -139,12 +139,9 @@ namespace osu.Game.Overlays.AccountCreation emailAddressDescription.AddText(AccountCreationStrings.EmailUsage); emailAddressDescription.AddText(AccountCreationStrings.MakeSureToGetIt, cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold)); - string[] passwordReq = localisationManager.GetLocalisedBindableString(AccountCreationStrings.PasswordRequirements("{}")).Value.Split("{}"); - if (passwordReq.Length != 2) passwordReq = AccountCreationStrings.PasswordRequirements("{}").ToString().Split("{}"); - - passwordDescription.AddText(passwordReq[0]); - characterCheckText = passwordDescription.AddText(AccountCreationStrings.CharactersLong); - passwordDescription.AddText(passwordReq[1]); + passwordDescription.AddText("At least "); + characterCheckText = passwordDescription.AddText("8 characters long"); + passwordDescription.AddText(". Choose something long but also something you will remember, like a line from your favourite song."); passwordTextBox.Current.BindValueChanged(_ => updateCharacterCheckTextColour(), true); characterCheckText.DrawablePartsRecreated += _ => updateCharacterCheckTextColour(); From 8cecfef2ff6bfe30d2f2af2419449eadbacd4235 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 14 Jun 2023 07:40:26 -0700 Subject: [PATCH 0185/2100] Apply key renaming suggestions --- osu.Game/Localisation/AccountCreationStrings.cs | 12 ++++++------ osu.Game/Overlays/AccountCreation/ScreenEntry.cs | 6 +++--- osu.Game/Overlays/AccountCreation/ScreenWarning.cs | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Localisation/AccountCreationStrings.cs b/osu.Game/Localisation/AccountCreationStrings.cs index 3884e4d8bf..282e458bb9 100644 --- a/osu.Game/Localisation/AccountCreationStrings.cs +++ b/osu.Game/Localisation/AccountCreationStrings.cs @@ -27,29 +27,29 @@ namespace osu.Game.Localisation /// /// "Help, I can't access my account!" /// - public static LocalisableString HelpICantAccess => new TranslatableString(getKey(@"help_icant_access"), @"Help, I can't access my account!"); + public static LocalisableString MultiAccountWarningHelp => new TranslatableString(getKey(@"multi_account_warning_help"), @"Help, I can't access my account!"); /// /// "I understand. This account isn't for me." /// - public static LocalisableString AccountIsntForMe => new TranslatableString(getKey(@"account_isnt_for_me"), @"I understand. This account isn't for me."); + public static LocalisableString MultiAccountWarningAccept => new TranslatableString(getKey(@"multi_account_warning_accept"), @"I understand. This account isn't for me."); /// /// "This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!" /// - public static LocalisableString ThisWillBeYourPublic => new TranslatableString(getKey(@"this_will_be_your_public"), + public static LocalisableString UsernameDescription => new TranslatableString(getKey(@"username_description"), @"This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!"); /// /// "Will be used for notifications, account verification and in the case you forget your password. No spam, ever." /// - public static LocalisableString EmailUsage => - new TranslatableString(getKey(@"email_usage"), @"Will be used for notifications, account verification and in the case you forget your password. No spam, ever."); + public static LocalisableString EmailDescription1 => + new TranslatableString(getKey(@"email_description_1"), @"Will be used for notifications, account verification and in the case you forget your password. No spam, ever."); /// /// " Make sure to get it right!" /// - public static LocalisableString MakeSureToGetIt => new TranslatableString(getKey(@"make_sure_to_get_it"), @" Make sure to get it right!"); + public static LocalisableString EmailDescription2 => new TranslatableString(getKey(@"email_description_2"), @" Make sure to get it right!"); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index ec3e7f893f..726fcc4304 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -134,10 +134,10 @@ namespace osu.Game.Overlays.AccountCreation textboxes = new[] { usernameTextBox, emailTextBox, passwordTextBox }; - usernameDescription.AddText(AccountCreationStrings.ThisWillBeYourPublic); + usernameDescription.AddText(AccountCreationStrings.UsernameDescription); - emailAddressDescription.AddText(AccountCreationStrings.EmailUsage); - emailAddressDescription.AddText(AccountCreationStrings.MakeSureToGetIt, cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold)); + emailAddressDescription.AddText(AccountCreationStrings.EmailDescription1); + emailAddressDescription.AddText(AccountCreationStrings.EmailDescription2, cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold)); passwordDescription.AddText("At least "); characterCheckText = passwordDescription.AddText("8 characters long"); diff --git a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs index f5807b49b5..0fbf6ba59e 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs @@ -102,13 +102,13 @@ namespace osu.Game.Overlays.AccountCreation }, new SettingsButton { - Text = AccountCreationStrings.HelpICantAccess, + Text = AccountCreationStrings.MultiAccountWarningHelp, Margin = new MarginPadding { Top = 50 }, Action = () => game?.OpenUrlExternally(help_centre_url) }, new DangerousSettingsButton { - Text = AccountCreationStrings.AccountIsntForMe, + Text = AccountCreationStrings.MultiAccountWarningAccept, Action = () => this.Push(new ScreenEntry()) }, furtherAssistance = new LinkFlowContainer(cp => cp.Font = cp.Font.With(size: 12)) From e4af1df6637e8a2f4fbc6613013d6cfadb81b0f3 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 14 Jun 2023 07:41:14 -0700 Subject: [PATCH 0186/2100] Apply automated formatting changes --- osu.Game/Localisation/AccountCreationStrings.cs | 6 ++---- osu.Game/Localisation/CommonStrings.cs | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Localisation/AccountCreationStrings.cs b/osu.Game/Localisation/AccountCreationStrings.cs index 282e458bb9..20ba7fe953 100644 --- a/osu.Game/Localisation/AccountCreationStrings.cs +++ b/osu.Game/Localisation/AccountCreationStrings.cs @@ -37,14 +37,12 @@ namespace osu.Game.Localisation /// /// "This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!" /// - public static LocalisableString UsernameDescription => new TranslatableString(getKey(@"username_description"), - @"This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!"); + public static LocalisableString UsernameDescription => new TranslatableString(getKey(@"username_description"), @"This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!"); /// /// "Will be used for notifications, account verification and in the case you forget your password. No spam, ever." /// - public static LocalisableString EmailDescription1 => - new TranslatableString(getKey(@"email_description_1"), @"Will be used for notifications, account verification and in the case you forget your password. No spam, ever."); + public static LocalisableString EmailDescription1 => new TranslatableString(getKey(@"email_description_1"), @"Will be used for notifications, account verification and in the case you forget your password. No spam, ever."); /// /// " Make sure to get it right!" diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 4ce05e96ef..ec78d34a16 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -166,4 +166,4 @@ namespace osu.Game.Localisation private static string getKey(string key) => $@"{prefix}:{key}"; } -} \ No newline at end of file +} From b54f3a2cba46e7073d2f5ba3ac798f8a3b732852 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 14 Jun 2023 07:58:55 -0700 Subject: [PATCH 0187/2100] Normalise source strings to sentence case The casing of the "caps lock" tooltip wasn't changed in code, as tooltips should be ideally sentence-cased, see https://github.com/ppy/osu/pull/21765#issuecomment-1552378930. --- osu.Game/Localisation/AccountCreationStrings.cs | 8 ++++---- osu.Game/Localisation/CommonStrings.cs | 4 ++-- osu.Game/Localisation/LoginPanelStrings.cs | 4 ++-- osu.Game/Overlays/AccountCreation/ScreenWelcome.cs | 5 +++-- osu.Game/Overlays/Login/LoginPanel.cs | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/osu.Game/Localisation/AccountCreationStrings.cs b/osu.Game/Localisation/AccountCreationStrings.cs index 20ba7fe953..2183df9b52 100644 --- a/osu.Game/Localisation/AccountCreationStrings.cs +++ b/osu.Game/Localisation/AccountCreationStrings.cs @@ -10,14 +10,14 @@ namespace osu.Game.Localisation private const string prefix = @"osu.Game.Resources.Localisation.AccountCreation"; /// - /// "New Player Registration" + /// "New player registration" /// - public static LocalisableString NewPlayerRegistration => new TranslatableString(getKey(@"new_player_registration"), @"New Player Registration"); + public static LocalisableString NewPlayerRegistration => new TranslatableString(getKey(@"new_player_registration"), @"New player registration"); /// - /// "let's get you started" + /// "Let's get you started" /// - public static LocalisableString LetsGetYouStarted => new TranslatableString(getKey(@"lets_get_you_started"), @"let's get you started"); + public static LocalisableString LetsGetYouStarted => new TranslatableString(getKey(@"lets_get_you_started"), @"Let's get you started"); /// /// "Let's create an account!" diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index ec78d34a16..c9223db246 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -155,9 +155,9 @@ namespace osu.Game.Localisation public static LocalisableString Exit => new TranslatableString(getKey(@"exit"), @"Exit"); /// - /// "caps lock is active" + /// "Caps lock is active" /// - public static LocalisableString CapsLockIsActive => new TranslatableString(getKey(@"caps_lock_is_active"), @"caps lock is active"); + public static LocalisableString CapsLockIsActive => new TranslatableString(getKey(@"caps_lock_is_active"), @"Caps lock is active"); /// /// "Revert to default" diff --git a/osu.Game/Localisation/LoginPanelStrings.cs b/osu.Game/Localisation/LoginPanelStrings.cs index 535d86fbc5..19b0ca3b52 100644 --- a/osu.Game/Localisation/LoginPanelStrings.cs +++ b/osu.Game/Localisation/LoginPanelStrings.cs @@ -25,9 +25,9 @@ namespace osu.Game.Localisation public static LocalisableString SignedIn => new TranslatableString(getKey(@"signed_in"), @"Signed in"); /// - /// "ACCOUNT" + /// "Account" /// - public static LocalisableString Account => new TranslatableString(getKey(@"account"), @"ACCOUNT"); + public static LocalisableString Account => new TranslatableString(getKey(@"account"), @"Account"); /// /// "Remember username" diff --git a/osu.Game/Overlays/AccountCreation/ScreenWelcome.cs b/osu.Game/Overlays/AccountCreation/ScreenWelcome.cs index a81b1019fe..610b9ee282 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenWelcome.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenWelcome.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; @@ -45,14 +46,14 @@ namespace osu.Game.Overlays.AccountCreation Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light), - Text = AccountCreationStrings.NewPlayerRegistration, + Text = AccountCreationStrings.NewPlayerRegistration.ToTitle(), }, new OsuSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Font = OsuFont.GetFont(size: 12), - Text = AccountCreationStrings.LetsGetYouStarted, + Text = AccountCreationStrings.LetsGetYouStarted.ToLower(), }, new SettingsButton { diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index fb9987bd82..79569ada65 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -82,7 +82,7 @@ namespace osu.Game.Overlays.Login { new OsuSpriteText { - Text = LoginPanelStrings.Account, + Text = LoginPanelStrings.Account.ToUpper(), Margin = new MarginPadding { Bottom = 5 }, Font = OsuFont.GetFont(weight: FontWeight.Bold), }, From 45e67fbe788bb34ac315ed1fb10e48d777c47cbe Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 14 Jun 2023 08:09:03 -0700 Subject: [PATCH 0188/2100] Remove unused load dependency --- osu.Game/Overlays/AccountCreation/ScreenEntry.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index 726fcc4304..9ad507d82a 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -10,7 +10,6 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Framework.Platform; using osu.Framework.Screens; @@ -53,7 +52,7 @@ namespace osu.Game.Overlays.AccountCreation private OsuGame game { get; set; } [BackgroundDependencyLoader] - private void load(LocalisationManager localisationManager) + private void load() { InternalChildren = new Drawable[] { From e9ef270e46b04a2d62d73106c9b3ae11d378c934 Mon Sep 17 00:00:00 2001 From: tsrk Date: Wed, 14 Jun 2023 19:39:28 +0200 Subject: [PATCH 0189/2100] refactor: move count logic in `InputTrigger` This will allow us to keep track of the real count regardless of whether the key counter has been placed mid-replay or not. --- osu.Game/Screens/Play/HUD/InputTrigger.cs | 30 ++++++++++++++++++++-- osu.Game/Screens/Play/HUD/KeyCounter.cs | 31 +++-------------------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/InputTrigger.cs b/osu.Game/Screens/Play/HUD/InputTrigger.cs index b57f2cdf91..edc61ec142 100644 --- a/osu.Game/Screens/Play/HUD/InputTrigger.cs +++ b/osu.Game/Screens/Play/HUD/InputTrigger.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 osu.Framework.Bindables; using osu.Framework.Graphics; namespace osu.Game.Screens.Play.HUD @@ -25,13 +26,38 @@ namespace osu.Game.Screens.Play.HUD public event OnActivateCallback? OnActivate; public event OnDeactivateCallback? OnDeactivate; + private readonly Bindable activationCount = new BindableInt(); + private readonly Bindable isCounting = new BindableBool(true); + + /// + /// Number of times this has been activated. + /// + public IBindable ActivationCount => activationCount; + + /// + /// Whether any activation or deactivation of this impacts its + /// + public IBindable IsCounting => isCounting; + protected InputTrigger(string name) { Name = name; } - protected void Activate(bool forwardPlayback = true) => OnActivate?.Invoke(forwardPlayback); + protected void Activate(bool forwardPlayback = true) + { + if (forwardPlayback && isCounting.Value) + activationCount.Value++; - protected void Deactivate(bool forwardPlayback = true) => OnDeactivate?.Invoke(forwardPlayback); + OnActivate?.Invoke(forwardPlayback); + } + + protected void Deactivate(bool forwardPlayback = true) + { + if (!forwardPlayback && isCounting.Value) + activationCount.Value--; + + OnDeactivate?.Invoke(forwardPlayback); + } } } diff --git a/osu.Game/Screens/Play/HUD/KeyCounter.cs b/osu.Game/Screens/Play/HUD/KeyCounter.cs index 7cdd6b025f..8074b30e75 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounter.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounter.cs @@ -22,15 +22,10 @@ namespace osu.Game.Screens.Play.HUD /// public Bindable IsCounting { get; } = new BindableBool(true); - private readonly Bindable countPresses = new BindableInt - { - MinValue = 0 - }; - /// /// The current count of registered key presses. /// - public IBindable CountPresses => countPresses; + public IBindable CountPresses => Trigger.ActivationCount; private readonly Container content; @@ -49,46 +44,28 @@ namespace osu.Game.Screens.Play.HUD { RelativeSizeAxes = Axes.Both }, - Trigger = trigger, }; + Trigger = trigger; + Trigger.OnActivate += Activate; Trigger.OnDeactivate += Deactivate; } - private void increment() - { - if (!IsCounting.Value) - return; - - countPresses.Value++; - } - - private void decrement() - { - if (!IsCounting.Value) - return; - - countPresses.Value--; - } - protected virtual void Activate(bool forwardPlayback = true) { IsActive.Value = true; - if (forwardPlayback) - increment(); } protected virtual void Deactivate(bool forwardPlayback = true) { IsActive.Value = false; - if (!forwardPlayback) - decrement(); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); + Trigger.OnActivate -= Activate; Trigger.OnDeactivate -= Deactivate; } From c637fddf73a4c7ead1cc9d1a534e82d0ecb351a0 Mon Sep 17 00:00:00 2001 From: tsrk Date: Wed, 14 Jun 2023 21:13:35 +0200 Subject: [PATCH 0190/2100] refactor: decouple Trigger logic from `KeyCounterDisplay` This allows to keep a coeherent state regardless of the progress of the play --- .../Visual/Gameplay/TestSceneAutoplay.cs | 7 +- .../Gameplay/TestSceneGameplayRewinding.cs | 8 +- .../Visual/Gameplay/TestSceneHUDOverlay.cs | 3 +- .../Visual/Gameplay/TestSceneKeyCounter.cs | 69 ++++++++------ .../Visual/Gameplay/TestSceneReplay.cs | 5 +- .../TestSceneSkinEditorMultipleSkins.cs | 7 ++ .../Gameplay/TestSceneSkinnableHUDOverlay.cs | 2 +- osu.Game/Rulesets/UI/RulesetInputManager.cs | 23 +++-- .../Screens/Play/HUD/KeyCounterController.cs | 95 +++++++++++++++++++ .../Screens/Play/HUD/KeyCounterDisplay.cs | 78 +++------------ osu.Game/Screens/Play/HUDOverlay.cs | 19 ++-- osu.Game/Screens/Play/Player.cs | 1 - 12 files changed, 180 insertions(+), 137 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/KeyCounterController.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index f628709db0..c829b73f66 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -13,7 +13,6 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.Break; -using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; using osu.Game.Users.Drawables; @@ -36,14 +35,14 @@ namespace osu.Game.Tests.Visual.Gameplay var referenceBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); - AddUntilStep("key counter counted keys", () => Player.HUDOverlay.ChildrenOfType().FirstOrDefault()?.Counters.Any(kc => kc.CountPresses.Value > 2) ?? false); + AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Triggers.Any(kc => kc.ActivationCount.Value > 2)); seekTo(referenceBeatmap.Breaks[0].StartTime); - AddAssert("keys not counting", () => !Player.HUDOverlay.ChildrenOfType().FirstOrDefault()?.IsCounting.Value ?? false); + AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting.Value); AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType().Single().AccuracyDisplay.Current.Value == 1); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); - AddUntilStep("key counter reset", () => Player.HUDOverlay.ChildrenOfType().FirstOrDefault()?.Counters.All(kc => kc.CountPresses.Value == 0) ?? false); + AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Triggers.All(kc => kc.ActivationCount.Value == 0)); seekTo(referenceBeatmap.HitObjects[^1].GetEndTime()); AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs index 3d920a3e92..508cf192d3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs @@ -7,13 +7,11 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Testing; -using osu.Framework.Utils; using osu.Framework.Timing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Screens.Play.HUD; using osu.Game.Storyboards; using osuTK; @@ -33,11 +31,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); addSeekStep(3000); AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged)); - AddUntilStep("key counter counted keys", () => Player.HUDOverlay.ChildrenOfType()?.FirstOrDefault()?.Counters.Select(kc => kc.CountPresses.Value).Sum() == 15); + AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Triggers.Select(kc => kc.ActivationCount.Value).Sum() == 15); AddStep("clear results", () => Player.Results.Clear()); addSeekStep(0); AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged)); - AddUntilStep("key counters reset", () => Player.HUDOverlay.ChildrenOfType().FirstOrDefault()?.Counters.All(kc => kc.CountPresses.Value == 0) ?? false); + AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Triggers.All(kc => kc.ActivationCount.Value == 0)); AddAssert("no results triggered", () => Player.Results.Count == 0); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 857d2b1d2d..bbb10c5957 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -6,7 +6,6 @@ using System.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -270,7 +269,7 @@ namespace osu.Game.Tests.Visual.Gameplay hudOverlay = new HUDOverlay(null, Array.Empty()); // Add any key just to display the key counter visually. - hudOverlay.ChildrenOfType().ForEach(k => k.Add(new KeyCounterKeyboardTrigger(Key.Space))); + hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); scoreProcessor.Combo.Value = 1; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 22f7111f68..7bf9738fb4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -5,7 +5,9 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; @@ -17,64 +19,69 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public partial class TestSceneKeyCounter : OsuManualInputManagerTestScene { + [Cached] + private readonly KeyCounterController controller; + + private readonly KeyCounterDisplay defaultDisplay; + public TestSceneKeyCounter() { - KeyCounterDisplay defaultDisplay = new DefaultKeyCounterDisplay + Children = new Drawable[] { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Position = new Vector2(0, 72.7f) + controller = new KeyCounterController(), + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(72.7f), + Children = new[] + { + defaultDisplay = new DefaultKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + }, + new ArgonKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + } + } + } }; - KeyCounterDisplay argonDisplay = new ArgonKeyCounterDisplay - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Position = new Vector2(0, -72.7f) - }; - - defaultDisplay.AddRange(new InputTrigger[] - { - new KeyCounterKeyboardTrigger(Key.X), - new KeyCounterKeyboardTrigger(Key.X), - new KeyCounterMouseTrigger(MouseButton.Left), - new KeyCounterMouseTrigger(MouseButton.Right), - }); - - argonDisplay.AddRange(new InputTrigger[] + controller.AddRange(new InputTrigger[] { new KeyCounterKeyboardTrigger(Key.X), new KeyCounterKeyboardTrigger(Key.X), new KeyCounterMouseTrigger(MouseButton.Left), new KeyCounterMouseTrigger(MouseButton.Right), }); + } + [Test] + public void TestDoThings() + { var testCounter = (DefaultKeyCounter)defaultDisplay.Counters.First(); AddStep("Add random", () => { Key key = (Key)((int)Key.A + RNG.Next(26)); - defaultDisplay.Add(new KeyCounterKeyboardTrigger(key)); - argonDisplay.Add(new KeyCounterKeyboardTrigger(key)); + controller.Add(new KeyCounterKeyboardTrigger(key)); }); - Key testKey = ((KeyCounterKeyboardTrigger)defaultDisplay.Counters.First().Trigger).Key; + Key testKey = ((KeyCounterKeyboardTrigger)controller.Triggers.First()).Key; addPressKeyStep(); AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 1); addPressKeyStep(); AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 2); - AddStep("Disable counting", () => - { - argonDisplay.IsCounting.Value = false; - defaultDisplay.IsCounting.Value = false; - }); + AddStep("Disable counting", () => controller.IsCounting.Value = false); addPressKeyStep(); AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses.Value == 2); - Add(defaultDisplay); - Add(argonDisplay); - void addPressKeyStep() => AddStep($"Press {testKey} key", () => InputManager.Key(testKey)); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs index 1fe7fc9063..5fb9bf004f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs @@ -6,13 +6,11 @@ using System; using System.ComponentModel; using System.Linq; -using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD; namespace osu.Game.Tests.Visual.Gameplay { @@ -29,8 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { AddUntilStep("score above zero", () => ((ScoreAccessibleReplayPlayer)Player).ScoreProcessor.TotalScore.Value > 0); - AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.ChildrenOfType().FirstOrDefault()?.Counters.Any(kc => kc.CountPresses - .Value > 0) ?? false); + AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Triggers.Any(kc => kc.ActivationCount.Value > 0)); AddAssert("cannot fail", () => !((ScoreAccessibleReplayPlayer)Player).AllowFail); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index 25cbe54cc3..ac772b980e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -14,7 +14,9 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Edit; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Tests.Gameplay; +using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { @@ -55,6 +57,11 @@ namespace osu.Game.Tests.Visual.Gameplay Origin = Anchor.Centre, }; + // Add any key just to display the key counter visually. + hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); + + scoreProcessor.Combo.Value = 1; + return new Container { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index 47f1ebf024..6532aa4ae5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Gameplay hudOverlay = new HUDOverlay(null, Array.Empty()); // Add any key just to display the key counter visually. - hudOverlay.ChildrenOfType().ForEach(k => k.Add(new KeyCounterKeyboardTrigger(Key.Space))); + hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); action?.Invoke(hudOverlay); diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 8065087341..294b72061b 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -164,9 +164,8 @@ namespace osu.Game.Rulesets.UI { switch (skinComponent) { - case KeyCounterDisplay keyCounterDisplay: - attachKeyCounter(keyCounterDisplay); - break; + case KeyCounterController keyCounterDisplay: + attachKeyCounter(keyCounterDisplay); break; case ClicksPerSecondCalculator clicksPerSecondCalculator: attachClicksPerSecond(clicksPerSecondCalculator); @@ -178,7 +177,7 @@ namespace osu.Game.Rulesets.UI { switch (skinComponent) { - case KeyCounterDisplay keyCounterDisplay: + case KeyCounterController keyCounterDisplay: detachKeyCounter(keyCounterDisplay); break; @@ -192,7 +191,7 @@ namespace osu.Game.Rulesets.UI #region Key Counter Attachment - private void attachKeyCounter(KeyCounterDisplay keyCounter) + private void attachKeyCounter(KeyCounterController keyCounter) { var receptor = new ActionReceptor(keyCounter); @@ -206,25 +205,25 @@ namespace osu.Game.Rulesets.UI .Select(action => new KeyCounterActionTrigger(action))); } - private void detachKeyCounter(KeyCounterDisplay keyCounter) + private void detachKeyCounter(KeyCounterController keyCounter) { + keyCounter.ClearReceptor(); } - private partial class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler + private partial class ActionReceptor : KeyCounterController.Receptor, IKeyBindingHandler { - public ActionReceptor(KeyCounterDisplay target) + public ActionReceptor(KeyCounterController target) : base(target) { } - public bool OnPressed(KeyBindingPressEvent e) => Target.Counters.Where(c => c.Trigger is KeyCounterActionTrigger) - .Select(c => (KeyCounterActionTrigger)c.Trigger) + public bool OnPressed(KeyBindingPressEvent e) => Target.Triggers + .OfType>() .Any(c => c.OnPressed(e.Action, Clock.Rate >= 0)); public void OnReleased(KeyBindingReleaseEvent e) { - foreach (var c - in Target.Counters.Where(c => c.Trigger is KeyCounterActionTrigger).Select(c => (KeyCounterActionTrigger)c.Trigger)) + foreach (var c in Target.Triggers.OfType>()) c.OnReleased(e.Action, Clock.Rate >= 0); } } diff --git a/osu.Game/Screens/Play/HUD/KeyCounterController.cs b/osu.Game/Screens/Play/HUD/KeyCounterController.cs new file mode 100644 index 0000000000..b138e64d6f --- /dev/null +++ b/osu.Game/Screens/Play/HUD/KeyCounterController.cs @@ -0,0 +1,95 @@ +// 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 osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Rulesets.UI; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class KeyCounterController : CompositeComponent, IAttachableSkinComponent + { + public readonly Bindable IsCounting = new BindableBool(true); + + private Receptor? receptor; + + public event Action? OnNewTrigger; + + private readonly Container triggers; + + public IReadOnlyList Triggers => triggers; + + public KeyCounterController() + { + InternalChild = triggers = new Container(); + } + + public void Add(InputTrigger trigger) + { + triggers.Add(trigger); + trigger.IsCounting.BindTo(IsCounting); + OnNewTrigger?.Invoke(trigger); + } + + public void AddRange(IEnumerable inputTriggers) => inputTriggers.ForEach(Add); + + /// + /// Sets a that will populate keybinding events to this . + /// + /// The receptor to set + /// When a is already active on this + public void SetReceptor(Receptor receptor) + { + if (this.receptor != null) + throw new InvalidOperationException("Cannot set a new receptor when one is already active"); + + this.receptor = receptor; + } + + /// + /// Clears any active + /// + public void ClearReceptor() + { + receptor = null; + } + + public override bool HandleNonPositionalInput => receptor == null; + + public override bool HandlePositionalInput => receptor == null; + + public partial class Receptor : Drawable + { + protected readonly KeyCounterController Target; + + public Receptor(KeyCounterController target) + { + RelativeSizeAxes = Axes.Both; + Depth = float.MinValue; + Target = target; + } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + + protected override bool Handle(UIEvent e) + { + switch (e) + { + case KeyDownEvent: + case KeyUpEvent: + case MouseDownEvent: + case MouseUpEvent: + return Target.TriggerEvent(e); + } + + return base.Handle(e); + } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs index b5c697ef13..b2d78216b2 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs @@ -1,18 +1,13 @@ // 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; using osu.Game.Configuration; using osu.Game.Rulesets.UI; -using osuTK; namespace osu.Game.Screens.Play.HUD { @@ -34,38 +29,13 @@ namespace osu.Game.Screens.Play.HUD protected abstract FillFlowContainer KeyFlow { get; } - /// - /// Whether the actions reported by all s within this should be counted. - /// - public Bindable IsCounting { get; } = new BindableBool(true); - protected readonly Bindable ConfigVisibility = new Bindable(); + [Resolved] + private KeyCounterController controller { get; set; } = null!; + protected abstract void UpdateVisibility(); - private Receptor? receptor; - - /// - /// Sets a that will populate keybinding events to this . - /// - /// The receptor to set - /// When a is already active on this - public void SetReceptor(Receptor receptor) - { - if (this.receptor != null) - throw new InvalidOperationException("Cannot set a new receptor when one is already active"); - - this.receptor = receptor; - } - - /// - /// Clears any active - /// - public void ClearReceptor() - { - receptor = null; - } - /// /// Add a to this display. /// @@ -74,8 +44,6 @@ namespace osu.Game.Screens.Play.HUD var keyCounter = CreateCounter(trigger); KeyFlow.Add(keyCounter); - - IsCounting.BindTo(keyCounter.IsCounting); } /// @@ -86,49 +54,29 @@ namespace osu.Game.Screens.Play.HUD protected abstract KeyCounter CreateCounter(InputTrigger trigger); [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, DrawableRuleset? drawableRuleset) { config.BindWith(OsuSetting.KeyOverlay, ConfigVisibility); + + if (drawableRuleset != null) + AlwaysVisible.BindTo(drawableRuleset.HasReplayLoaded); } protected override void LoadComplete() { base.LoadComplete(); + controller.OnNewTrigger += Add; + AddRange(controller.Triggers); + AlwaysVisible.BindValueChanged(_ => UpdateVisibility()); ConfigVisibility.BindValueChanged(_ => UpdateVisibility(), true); } - public override bool HandleNonPositionalInput => receptor == null; - - public override bool HandlePositionalInput => receptor == null; - - public partial class Receptor : Drawable + protected override void Dispose(bool isDisposing) { - protected readonly KeyCounterDisplay Target; - - public Receptor(KeyCounterDisplay target) - { - RelativeSizeAxes = Axes.Both; - Depth = float.MinValue; - Target = target; - } - - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - - protected override bool Handle(UIEvent e) - { - switch (e) - { - case KeyDownEvent: - case KeyUpEvent: - case MouseDownEvent: - case MouseUpEvent: - return Target.InternalChildren.Any(c => c.TriggerEvent(e)); - } - - return base.Handle(e); - } + base.Dispose(isDisposing); + controller.OnNewTrigger -= Add; } } } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index ae0e67867b..15a0e0688b 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -16,8 +16,10 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Configuration; using osu.Game.Input.Bindings; +using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -26,8 +28,6 @@ using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osu.Game.Screens.Play.HUD.JudgementCounter; using osu.Game.Skinning; using osuTK; -using osu.Game.Localisation; -using osu.Game.Rulesets; namespace osu.Game.Screens.Play { @@ -54,7 +54,6 @@ namespace osu.Game.Screens.Play return child == bottomRightElements; } - public readonly KeyCounterDisplay KeyCounter; public readonly ModDisplay ModDisplay; public readonly HoldForMenuButton HoldToQuit; public readonly PlayerSettingsOverlay PlayerSettingsOverlay; @@ -62,6 +61,9 @@ namespace osu.Game.Screens.Play [Cached] private readonly ClicksPerSecondCalculator clicksPerSecondCalculator; + [Cached] + public readonly KeyCounterController KeyCounter; + [Cached] private readonly JudgementTally tally; @@ -145,7 +147,6 @@ namespace osu.Game.Screens.Play Direction = FillDirection.Vertical, Children = new Drawable[] { - KeyCounter = CreateKeyCounter(), HoldToQuit = CreateHoldForMenuButton(), } }, @@ -157,9 +158,10 @@ namespace osu.Game.Screens.Play Spacing = new Vector2(5) }, clicksPerSecondCalculator = new ClicksPerSecondCalculator(), + KeyCounter = new KeyCounterController() }; - hideTargets = new List { mainComponents, rulesetComponents, KeyCounter, topRightElements }; + hideTargets = new List { mainComponents, rulesetComponents, topRightElements }; if (!alwaysShowLeaderboard) hideTargets.Add(LeaderboardFlow); @@ -321,7 +323,6 @@ namespace osu.Game.Screens.Play { attachTarget.Attach(KeyCounter); attachTarget.Attach(clicksPerSecondCalculator); - mainComponents.SetAttachTarget(attachTarget); } replayLoaded.BindTo(drawableRuleset.HasReplayLoaded); @@ -332,12 +333,6 @@ namespace osu.Game.Screens.Play ShowHealth = { BindTarget = ShowHealthBar } }; - protected KeyCounterDisplay CreateKeyCounter() => new DefaultKeyCounterDisplay - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - }; - protected HoldForMenuButton CreateHoldForMenuButton() => new HoldForMenuButton { Anchor = Anchor.BottomRight, diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 18ea9d0acb..9fc97162bf 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -438,7 +438,6 @@ namespace osu.Game.Screens.Play { Value = false }, - AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, }, Anchor = Anchor.Centre, Origin = Anchor.Centre From e26aeea589ffa3ad4bf5a3a391a3574feb8a36ad Mon Sep 17 00:00:00 2001 From: tsrk Date: Wed, 14 Jun 2023 21:15:12 +0200 Subject: [PATCH 0191/2100] feat: make `KeyCounterDisplay` skinnable --- osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs | 5 ++++- osu.Game/Skinning/ArgonSkin.cs | 10 ++++++++++ osu.Game/Skinning/TrianglesSkin.cs | 10 ++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs index b2d78216b2..d599d383a5 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs @@ -8,13 +8,14 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Rulesets.UI; +using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD { /// /// A flowing display of all gameplay keys. Individual keys can be added using implementations. /// - public abstract partial class KeyCounterDisplay : CompositeDrawable, IAttachableSkinComponent + public abstract partial class KeyCounterDisplay : CompositeDrawable, IAttachableSkinComponent, ISerialisableDrawable { /// /// Whether the key counter should be visible regardless of the configuration value. @@ -78,5 +79,7 @@ namespace osu.Game.Screens.Play.HUD base.Dispose(isDisposing); controller.OnNewTrigger -= Add; } + + public bool UsesFixedAnchor { get; set; } } } diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index a9b26f13e8..48326bfe60 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -12,6 +12,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps.Formats; using osu.Game.Extensions; using osu.Game.IO; +using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; using osuTK; @@ -113,6 +114,7 @@ namespace osu.Game.Skinning var combo = container.OfType().FirstOrDefault(); var ppCounter = container.OfType().FirstOrDefault(); var songProgress = container.OfType().FirstOrDefault(); + var keyCounter = container.OfType().FirstOrDefault(); if (score != null) { @@ -168,6 +170,13 @@ namespace osu.Game.Skinning { songProgress.Position = new Vector2(0, -10); songProgress.Scale = new Vector2(0.9f, 1); + + if (keyCounter != null) + { + keyCounter.Anchor = Anchor.BottomLeft; + keyCounter.Origin = Anchor.BottomLeft; + keyCounter.Position = new Vector2(50, -57); + } } } }) @@ -179,6 +188,7 @@ namespace osu.Game.Skinning new DefaultAccuracyCounter(), new DefaultHealthDisplay(), new ArgonSongProgress(), + new ArgonKeyCounterDisplay(), new BarHitErrorMeter(), new BarHitErrorMeter(), new PerformancePointsCounter() diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index e88b827807..bb562468db 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -90,6 +90,8 @@ namespace osu.Game.Skinning var accuracy = container.OfType().FirstOrDefault(); var combo = container.OfType().FirstOrDefault(); var ppCounter = container.OfType().FirstOrDefault(); + var songProgress = container.OfType().FirstOrDefault(); + var keyCounter = container.OfType().FirstOrDefault(); if (score != null) { @@ -141,6 +143,13 @@ namespace osu.Game.Skinning hitError2.Origin = Anchor.CentreLeft; } } + + if (songProgress != null && keyCounter != null) + { + keyCounter.Anchor = Anchor.BottomRight; + keyCounter.Origin = Anchor.BottomRight; + keyCounter.Position = new Vector2(10, songProgress.Height + 10); + } }) { Children = new Drawable[] @@ -150,6 +159,7 @@ namespace osu.Game.Skinning new DefaultAccuracyCounter(), new DefaultHealthDisplay(), new DefaultSongProgress(), + new DefaultKeyCounterDisplay(), new BarHitErrorMeter(), new BarHitErrorMeter(), new PerformancePointsCounter() From 081190802e3f599b93559b84220909b9638130fe Mon Sep 17 00:00:00 2001 From: tsrk Date: Wed, 14 Jun 2023 21:19:08 +0200 Subject: [PATCH 0192/2100] revert: remove attachment logic from `SkinComponentContainer` This is no longer in the scope of the PR. --- osu.Game/Skinning/SkinComponentsContainer.cs | 26 -------------------- 1 file changed, 26 deletions(-) diff --git a/osu.Game/Skinning/SkinComponentsContainer.cs b/osu.Game/Skinning/SkinComponentsContainer.cs index 19c16d2177..adf0a288b4 100644 --- a/osu.Game/Skinning/SkinComponentsContainer.cs +++ b/osu.Game/Skinning/SkinComponentsContainer.cs @@ -6,10 +6,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.UI; namespace osu.Game.Skinning { @@ -41,8 +39,6 @@ namespace osu.Game.Skinning private CancellationTokenSource? cancellationSource; - private ICanAttachHUDPieces? attachTarget; - public SkinComponentsContainer(SkinComponentsContainerLookup lookup) { Lookup = lookup; @@ -66,10 +62,6 @@ namespace osu.Game.Skinning public void Reload(Container? componentsContainer) { - components - .OfType() - .ForEach(c => attachTarget?.Detach(c)); - ClearInternal(); components.Clear(); ComponentsLoaded = false; @@ -85,7 +77,6 @@ namespace osu.Game.Skinning LoadComponentAsync(content, wrapper => { AddInternal(wrapper); - wrapper.Children.OfType().ForEach(c => attachTarget?.Attach(c)); components.AddRange(wrapper.Children.OfType()); ComponentsLoaded = true; }, (cancellationSource = new CancellationTokenSource()).Token); @@ -102,9 +93,6 @@ namespace osu.Game.Skinning if (!(component is Drawable drawable)) throw new ArgumentException($"Provided argument must be of type {nameof(Drawable)}.", nameof(component)); - if (component is IAttachableSkinComponent attachableSkinComponent) - attachTarget?.Attach(attachableSkinComponent); - content.Add(drawable); components.Add(component); } @@ -120,24 +108,10 @@ namespace osu.Game.Skinning if (!(component is Drawable drawable)) throw new ArgumentException($"Provided argument must be of type {nameof(Drawable)}.", nameof(component)); - if (component is IAttachableSkinComponent attachableSkinComponent) - attachTarget?.Detach(attachableSkinComponent); - content.Remove(drawable, disposeImmediately); components.Remove(component); } - public void SetAttachTarget(ICanAttachHUDPieces target) - { - attachTarget = target; - - foreach (var child in InternalChildren) - { - if (child is IAttachableSkinComponent attachable) - attachTarget.Attach(attachable); - } - } - protected override void SkinChanged(ISkinSource skin) { base.SkinChanged(skin); From 4cc2bb0c7db35e4b52ac230e81cb5f50a08dfbb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 14 Jun 2023 21:33:11 +0200 Subject: [PATCH 0193/2100] Rename things in test to match --- osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs index 0ffbeeb491..94086f10f2 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs @@ -134,7 +134,7 @@ namespace osu.Game.Tournament.Tests.Screens } [Test] - public void TestDisableMapsPerMod() + public void TestSplitMapPoolByMods() { AddStep("load many maps", () => { @@ -144,7 +144,7 @@ namespace osu.Game.Tournament.Tests.Screens addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM"); }); - AddStep("disable maps per mod", () => Ladder.SplitMapPoolByMods.Value = false); + AddStep("disable splitting map pool by mods", () => Ladder.SplitMapPoolByMods.Value = false); AddStep("reset match", () => { From ccf6ed1e5b58f8b6810f589969d752f4681d3fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 14 Jun 2023 18:10:49 +0200 Subject: [PATCH 0194/2100] Add failing test cases --- .../TestSceneMaximumScore.cs | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs new file mode 100644 index 0000000000..3d0abaceb5 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs @@ -0,0 +1,147 @@ +// 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 NUnit.Framework; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public partial class TestSceneMaximumScore : RateAdjustedBeatmapTestScene + { + private ScoreAccessibleReplayPlayer currentPlayer = null!; + + private List judgementResults = new List(); + + [Test] + public void TestSimultaneousTickAndNote() + { + performTest( + new List + { + new HoldNote + { + StartTime = 1000, + Duration = 2000, + Column = 0, + }, + new Note + { + StartTime = 2000, + Column = 1 + } + }, + new List + { + new ManiaReplayFrame(1000, ManiaAction.Key1), + new ManiaReplayFrame(2000, ManiaAction.Key1, ManiaAction.Key2), + new ManiaReplayFrame(2001, ManiaAction.Key1), + new ManiaReplayFrame(3000) + }); + + AddAssert("all objects perfectly judged", + () => judgementResults.Select(result => result.Type), + () => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult))); + AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000)); + } + + [Test] + public void TestSimultaneousLongNotes() + { + performTest( + new List + { + new HoldNote + { + StartTime = 1000, + Duration = 2000, + Column = 0, + }, + new HoldNote + { + StartTime = 2000, + Duration = 2000, + Column = 1 + } + }, + new List + { + new ManiaReplayFrame(1000, ManiaAction.Key1), + new ManiaReplayFrame(2000, ManiaAction.Key1, ManiaAction.Key2), + new ManiaReplayFrame(3000, ManiaAction.Key2), + new ManiaReplayFrame(4000) + }); + + AddAssert("all objects perfectly judged", + () => judgementResults.Select(result => result.Type), + () => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult))); + AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000)); + } + + private void performTest(List hitObjects, List frames) + { + var beatmap = new Beatmap + { + HitObjects = hitObjects, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty { SliderTickRate = 4 }, + Ruleset = new ManiaRuleset().RulesetInfo + }, + }; + + beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f }); + + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(beatmap); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + }; + + LoadScreen(currentPlayer = p); + judgementResults = new List(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + } + + private partial class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + } + } + } +} From 04e812b5ab6f2098e548ca8f292053a2f84d2704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 14 Jun 2023 20:08:14 +0200 Subject: [PATCH 0195/2100] Make `JudgementProcessor.SimulateAutoplay()` non-virtual Nobody overrides this, and with the structure given, overriders would have to rewrite half of this code anyway. The fact that the class has 2 other overridable members (`CreateResult()`, `GetSimulatedHitResult()`) which cease to have any meaning if `SimulateAutoplay()` is overridden also contributes to taking this decision. --- osu.Game/Rulesets/Scoring/JudgementProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index b16c307206..181fbef405 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Scoring /// /// This provided temporarily. DO NOT USE. /// The to simulate. - protected virtual void SimulateAutoplay(IBeatmap beatmap) + protected void SimulateAutoplay(IBeatmap beatmap) { IsSimulating = true; From 3295294cbc4cc324dd8281c8ed1258ea7eeed0ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 14 Jun 2023 20:10:34 +0200 Subject: [PATCH 0196/2100] Reorder autoplay-related virtual methods closer together --- .../Rulesets/Scoring/JudgementProcessor.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 181fbef405..9c86cbfe90 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -137,13 +137,6 @@ namespace osu.Game.Rulesets.Scoring JudgedHits += count; } - /// - /// Creates the that represents the scoring result for a . - /// - /// The which was judged. - /// The that provides the scoring information. - protected virtual JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new JudgementResult(hitObject, judgement); - /// /// Simulates an autoplay of the to determine scoring values. /// @@ -174,6 +167,20 @@ namespace osu.Game.Rulesets.Scoring IsSimulating = false; } + /// + /// Creates the that represents the scoring result for a . + /// + /// The which was judged. + /// The that provides the scoring information. + protected virtual JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new JudgementResult(hitObject, judgement); + + /// + /// Gets a simulated for a judgement. Used during to simulate a "perfect" play. + /// + /// The judgement to simulate a for. + /// The simulated for the judgement. + protected virtual HitResult GetSimulatedHitResult(Judgement judgement) => judgement.MaxResult; + protected override void Update() { base.Update(); @@ -184,12 +191,5 @@ namespace osu.Game.Rulesets.Scoring // Last applied result is guaranteed to be non-null when JudgedHits > 0. || lastAppliedResult.AsNonNull().TimeAbsolute < Clock.CurrentTime); } - - /// - /// Gets a simulated for a judgement. Used during to simulate a "perfect" play. - /// - /// The judgement to simulate a for. - /// The simulated for the judgement. - protected virtual HitResult GetSimulatedHitResult(Judgement judgement) => judgement.MaxResult; } } From 462570801a1441580af32e049400c2f682fb876d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 14 Jun 2023 20:15:43 +0200 Subject: [PATCH 0197/2100] Introduce new method for enumeration of objects during autoplay simulation --- .../Rulesets/Scoring/JudgementProcessor.cs | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 9c86cbfe90..2c42a08864 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.TypeExtensions; @@ -146,14 +147,11 @@ namespace osu.Game.Rulesets.Scoring { IsSimulating = true; - foreach (var obj in beatmap.HitObjects) + foreach (var obj in EnumerateHitObjects(beatmap)) simulate(obj); void simulate(HitObject obj) { - foreach (var nested in obj.NestedHitObjects) - simulate(nested); - var judgement = obj.CreateJudgement(); var result = CreateResult(obj, judgement); @@ -167,6 +165,29 @@ namespace osu.Game.Rulesets.Scoring IsSimulating = false; } + /// + /// Enumerates all s in the given in the order in which they are to be judged. + /// Used in . + /// + /// + /// In Score V2, the score awarded for each object includes a component based on the combo value after the judgement of that object. + /// This means that the score is dependent on the order of evaluation of judgements. + /// This method is provided so that rulesets can specify custom ordering that is correct for them and matches processing order during actual gameplay. + /// + protected virtual IEnumerable EnumerateHitObjects(IBeatmap beatmap) + => enumerateRecursively(beatmap.HitObjects); + + private IEnumerable enumerateRecursively(IEnumerable hitObjects) + { + foreach (var hitObject in hitObjects) + { + foreach (var nested in enumerateRecursively(hitObject.NestedHitObjects)) + yield return nested; + + yield return hitObject; + } + } + /// /// Creates the that represents the scoring result for a . /// From e46b4209c3efecf10143bf2b7ebb4a4b6bbe821d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 14 Jun 2023 20:35:49 +0200 Subject: [PATCH 0198/2100] Remove no-longer-needed local method --- osu.Game/Rulesets/Scoring/JudgementProcessor.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 2c42a08864..e9f3bcb949 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -148,9 +148,6 @@ namespace osu.Game.Rulesets.Scoring IsSimulating = true; foreach (var obj in EnumerateHitObjects(beatmap)) - simulate(obj); - - void simulate(HitObject obj) { var judgement = obj.CreateJudgement(); From 0065334241f77b703c1c7523ab0e917a4ae53fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 14 Jun 2023 20:34:06 +0200 Subject: [PATCH 0199/2100] Fix mania autoplay simulation judging objects in different order to gameplay --- .../Scoring/ManiaScoreProcessor.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 6292ed75cd..a0f6ac572d 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -2,7 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Scoring @@ -16,6 +21,9 @@ namespace osu.Game.Rulesets.Mania.Scoring { } + protected override IEnumerable EnumerateHitObjects(IBeatmap beatmap) + => base.EnumerateHitObjects(beatmap).OrderBy(ho => (ManiaHitObject)ho, JudgementOrderComparer.DEFAULT); + protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) { return 10000 * comboProgress @@ -25,5 +33,27 @@ namespace osu.Game.Rulesets.Mania.Scoring protected override double GetComboScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base)); + + private class JudgementOrderComparer : IComparer + { + public static readonly JudgementOrderComparer DEFAULT = new JudgementOrderComparer(); + + public int Compare(ManiaHitObject? x, ManiaHitObject? y) + { + if (ReferenceEquals(x, y)) return 0; + if (ReferenceEquals(x, null)) return -1; + if (ReferenceEquals(y, null)) return 1; + + int result = x.GetEndTime().CompareTo(y.GetEndTime()); + if (result != 0) + return result; + + // due to the way input is handled in mania, notes take precedence over ticks in judging order. + if (x is Note && y is not Note) return -1; + if (x is not Note && y is Note) return 1; + + return x.Column.CompareTo(y.Column); + } + } } } From 0ddd43d44df5e2f5c78b0be670c1ce978f59824a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 15 Jun 2023 15:15:32 +0900 Subject: [PATCH 0200/2100] Update CFS to NET6.0 --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 1f937e1837..8c8a3be771 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -15,7 +15,7 @@ ] }, "codefilesanity": { - "version": "0.0.36", + "version": "0.0.37", "commands": [ "CodeFileSanity" ] From b9543f4fddb173701fc1e110d25c5522d85b22fe Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 15 Jun 2023 11:05:06 +0300 Subject: [PATCH 0201/2100] Add failing test case --- .../SongSelect/TestSceneBeatmapCarousel.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 61a8322ee3..61f95dc628 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -453,6 +453,25 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false)); } + [Test] + public void TestRewindToDeletedBeatmap() + { + loadBeatmaps(); + + var firstAdded = TestResources.CreateTestBeatmapSetInfo(); + + AddStep("add new set", () => carousel.UpdateBeatmapSet(firstAdded)); + AddStep("select set", () => carousel.SelectBeatmap(firstAdded.Beatmaps.First())); + + nextRandom(); + + AddStep("delete set", () => carousel.RemoveBeatmapSet(firstAdded)); + + prevRandom(); + + AddAssert("deleted set not selected", () => carousel.SelectedBeatmapSet?.Equals(firstAdded) == false); + } + /// /// Test adding and removing beatmap sets /// From 39db17d2e97c20f3d56c55113514ac80724aa5f5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 15 Jun 2023 11:22:11 +0300 Subject: [PATCH 0202/2100] Use better method to avoid rewinding to deleted beatmaps --- osu.Game/Screens/Select/BeatmapCarousel.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 8c0f67564a..3d87a57295 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -155,7 +155,7 @@ namespace osu.Game.Screens.Select public Bindable RandomAlgorithm = new Bindable(); private readonly List previouslyVisitedRandomSets = new List(); - private readonly Stack randomSelectedBeatmaps = new Stack(); + private readonly List randomSelectedBeatmaps = new List(); private CarouselRoot root; @@ -348,6 +348,11 @@ namespace osu.Game.Screens.Select if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSet)) return; + foreach (var beatmap in existingSet.Beatmaps) + randomSelectedBeatmaps.Remove(beatmap); + + previouslyVisitedRandomSets.Remove(existingSet); + root.RemoveItem(existingSet); itemsCache.Invalidate(); @@ -501,7 +506,7 @@ namespace osu.Game.Screens.Select if (selectedBeatmap != null && selectedBeatmapSet != null) { - randomSelectedBeatmaps.Push(selectedBeatmap); + randomSelectedBeatmaps.Add(selectedBeatmap); // when performing a random, we want to add the current set to the previously visited list // else the user may be "randomised" to the existing selection. @@ -538,9 +543,10 @@ namespace osu.Game.Screens.Select { while (randomSelectedBeatmaps.Any()) { - var beatmap = randomSelectedBeatmaps.Pop(); + var beatmap = randomSelectedBeatmaps[^1]; + randomSelectedBeatmaps.Remove(beatmap); - if (!beatmap.Filtered.Value && beatmapSets.Any(beatset => beatset.Beatmaps.Contains(beatmap))) + if (!beatmap.Filtered.Value && beatmap.BeatmapInfo.BeatmapSet?.DeletePending != true) { if (selectedBeatmapSet != null) { From 2f40f5bd1969863f86930beab52e031e35efd917 Mon Sep 17 00:00:00 2001 From: tsrk Date: Thu, 15 Jun 2023 12:01:38 +0200 Subject: [PATCH 0203/2100] fix: change key counter position in Triangles and Legacy skins --- osu.Game/Skinning/LegacySkin.cs | 10 ++++++++++ osu.Game/Skinning/TrianglesSkin.cs | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index e46eaf90c1..e35f8fbe4d 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -22,6 +22,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; +using osuTK; using osuTK.Graphics; namespace osu.Game.Skinning @@ -372,12 +373,20 @@ namespace osu.Game.Skinning } var hitError = container.OfType().FirstOrDefault(); + var keyCounter = container.OfType().FirstOrDefault(); if (hitError != null) { hitError.Anchor = Anchor.BottomCentre; hitError.Origin = Anchor.CentreLeft; hitError.Rotation = -90; + + if (keyCounter != null) + { + keyCounter.Anchor = Anchor.BottomRight; + keyCounter.Origin = Anchor.BottomRight; + keyCounter.Position = new Vector2(10, -10 - hitError.Width); + } } }) { @@ -389,6 +398,7 @@ namespace osu.Game.Skinning new LegacyHealthDisplay(), new LegacySongProgress(), new BarHitErrorMeter(), + new DefaultKeyCounterDisplay() } }; } diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index bb562468db..424c477bda 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -148,7 +148,7 @@ namespace osu.Game.Skinning { keyCounter.Anchor = Anchor.BottomRight; keyCounter.Origin = Anchor.BottomRight; - keyCounter.Position = new Vector2(10, songProgress.Height + 10); + keyCounter.Position = new Vector2(10, -57 - 10); } }) { From 7e705987738729ddbb01de3554d99de24f0f7a87 Mon Sep 17 00:00:00 2001 From: tsrk Date: Thu, 15 Jun 2023 12:02:30 +0200 Subject: [PATCH 0204/2100] test: move back key counter tests in ctor --- .../Visual/Gameplay/TestSceneKeyCounter.cs | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 7bf9738fb4..d7c2b1c7dd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -22,8 +22,6 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private readonly KeyCounterController controller; - private readonly KeyCounterDisplay defaultDisplay; - public TestSceneKeyCounter() { Children = new Drawable[] @@ -36,9 +34,9 @@ namespace osu.Game.Tests.Visual.Gameplay RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, Spacing = new Vector2(72.7f), - Children = new[] + Children = new KeyCounterDisplay[] { - defaultDisplay = new DefaultKeyCounterDisplay + new DefaultKeyCounterDisplay { Origin = Anchor.Centre, Anchor = Anchor.Centre, @@ -59,12 +57,6 @@ namespace osu.Game.Tests.Visual.Gameplay new KeyCounterMouseTrigger(MouseButton.Left), new KeyCounterMouseTrigger(MouseButton.Right), }); - } - - [Test] - public void TestDoThings() - { - var testCounter = (DefaultKeyCounter)defaultDisplay.Counters.First(); AddStep("Add random", () => { @@ -72,15 +64,16 @@ namespace osu.Game.Tests.Visual.Gameplay controller.Add(new KeyCounterKeyboardTrigger(key)); }); - Key testKey = ((KeyCounterKeyboardTrigger)controller.Triggers.First()).Key; + InputTrigger testTrigger = controller.Triggers.First(); + Key testKey = ((KeyCounterKeyboardTrigger)testTrigger).Key; addPressKeyStep(); - AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 1); + AddAssert($"Check {testKey} counter after keypress", () => testTrigger.ActivationCount.Value == 1); addPressKeyStep(); - AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 2); + AddAssert($"Check {testKey} counter after keypress", () => testTrigger.ActivationCount.Value == 2); AddStep("Disable counting", () => controller.IsCounting.Value = false); addPressKeyStep(); - AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses.Value == 2); + AddAssert($"Check {testKey} count has not changed", () => testTrigger.ActivationCount.Value == 2); void addPressKeyStep() => AddStep($"Press {testKey} key", () => InputManager.Key(testKey)); } From b4cbcb210e71ea874de76e73a295940b3cf442fd Mon Sep 17 00:00:00 2001 From: tsrk Date: Thu, 15 Jun 2023 12:24:37 +0200 Subject: [PATCH 0205/2100] refactor: remove detachment logic No real use case, cleaning up the diff --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 3 --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 27 ++------------------- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index d899b6bad1..4fa18f53a7 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -330,9 +330,6 @@ namespace osu.Game.Rulesets.UI public void Attach(IAttachableSkinComponent skinComponent) => (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(skinComponent); - public void Detach(IAttachableSkinComponent skinComponent) => - (KeyBindingInputManager as ICanAttachHUDPieces)?.Detach(skinComponent); - /// /// Creates a key conversion input manager. An exception will be thrown if a valid is not returned. /// diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 294b72061b..889890e711 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -165,7 +165,8 @@ namespace osu.Game.Rulesets.UI switch (skinComponent) { case KeyCounterController keyCounterDisplay: - attachKeyCounter(keyCounterDisplay); break; + attachKeyCounter(keyCounterDisplay); + break; case ClicksPerSecondCalculator clicksPerSecondCalculator: attachClicksPerSecond(clicksPerSecondCalculator); @@ -173,20 +174,6 @@ namespace osu.Game.Rulesets.UI } } - public void Detach(IAttachableSkinComponent skinComponent) - { - switch (skinComponent) - { - case KeyCounterController keyCounterDisplay: - detachKeyCounter(keyCounterDisplay); - break; - - case ClicksPerSecondCalculator clicksPerSecondCalculator: - detachClicksPerSecond(clicksPerSecondCalculator); - break; - } - } - #endregion #region Key Counter Attachment @@ -205,11 +192,6 @@ namespace osu.Game.Rulesets.UI .Select(action => new KeyCounterActionTrigger(action))); } - private void detachKeyCounter(KeyCounterController keyCounter) - { - keyCounter.ClearReceptor(); - } - private partial class ActionReceptor : KeyCounterController.Receptor, IKeyBindingHandler { public ActionReceptor(KeyCounterController target) @@ -239,10 +221,6 @@ namespace osu.Game.Rulesets.UI KeyBindingContainer.Add(listener); } - private void detachClicksPerSecond(ClicksPerSecondCalculator calculator) - { - } - private partial class ActionListener : Component, IKeyBindingHandler { private readonly ClicksPerSecondCalculator calculator; @@ -306,7 +284,6 @@ namespace osu.Game.Rulesets.UI public interface ICanAttachHUDPieces { void Attach(IAttachableSkinComponent component); - void Detach(IAttachableSkinComponent component); } public interface IAttachableSkinComponent From a61c1116f5f5d1d2b87e8176390e7c9338b88c87 Mon Sep 17 00:00:00 2001 From: tsrk Date: Thu, 15 Jun 2023 12:27:37 +0200 Subject: [PATCH 0206/2100] style: remove unused IAttachableSkinComponent on KCD --- osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs index d599d383a5..8b92a5e3b8 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.Play.HUD /// /// A flowing display of all gameplay keys. Individual keys can be added using implementations. /// - public abstract partial class KeyCounterDisplay : CompositeDrawable, IAttachableSkinComponent, ISerialisableDrawable + public abstract partial class KeyCounterDisplay : CompositeDrawable, ISerialisableDrawable { /// /// Whether the key counter should be visible regardless of the configuration value. From 184c793f568f568e9d855656d7c3b91a355976b1 Mon Sep 17 00:00:00 2001 From: tsrk Date: Thu, 15 Jun 2023 12:33:26 +0200 Subject: [PATCH 0207/2100] style(KeyCounter): remove useless `Content` override --- osu.Game/Screens/Play/HUD/KeyCounter.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/KeyCounter.cs b/osu.Game/Screens/Play/HUD/KeyCounter.cs index 8074b30e75..7b99c34a55 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounter.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounter.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; namespace osu.Game.Screens.Play.HUD @@ -27,10 +26,6 @@ namespace osu.Game.Screens.Play.HUD /// public IBindable CountPresses => Trigger.ActivationCount; - private readonly Container content; - - protected override Container Content => content; - /// /// Whether this is currently in the "activated" state because the associated key is currently pressed. /// @@ -38,14 +33,6 @@ namespace osu.Game.Screens.Play.HUD protected KeyCounter(InputTrigger trigger) { - InternalChildren = new Drawable[] - { - content = new Container - { - RelativeSizeAxes = Axes.Both - }, - }; - Trigger = trigger; Trigger.OnActivate += Activate; From fcdaf729158cf3461d64c784f47930487b24a8b6 Mon Sep 17 00:00:00 2001 From: tsrk Date: Thu, 15 Jun 2023 12:40:47 +0200 Subject: [PATCH 0208/2100] style(KeyCounter): remove useless `IsCounting` bindable Counting logic has been moved to the `Trigger` --- osu.Game/Screens/Play/HUD/KeyCounter.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/KeyCounter.cs b/osu.Game/Screens/Play/HUD/KeyCounter.cs index 7b99c34a55..f12d2166fc 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounter.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounter.cs @@ -16,11 +16,6 @@ namespace osu.Game.Screens.Play.HUD /// public readonly InputTrigger Trigger; - /// - /// Whether the actions reported by should be counted. - /// - public Bindable IsCounting { get; } = new BindableBool(true); - /// /// The current count of registered key presses. /// From 975e9baf432dcb2af21869ad5f5e7fa47bfce34d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 15 Jun 2023 19:55:51 +0900 Subject: [PATCH 0209/2100] Fix exception with no matching mods --- osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs index 5712205e8f..f28a86b6b4 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty { double multiplier = mods.Where(m => m is not (ModHidden or ModHardRock or ModDoubleTime or ModFlashlight or ManiaModFadeIn)) .Select(m => m.ScoreMultiplier) - .Aggregate((c, n) => c * n); + .Aggregate(1.0, (c, n) => c * n); TotalScore = (int)(1000000 * multiplier); } From 145530035cccad94947aba2e4c8ba78d3c18c817 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 15 Jun 2023 20:00:15 +0900 Subject: [PATCH 0210/2100] Optimise mania density calculation during beatmap conversion --- osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 632b7cdcc7..bdc5a00583 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -119,14 +119,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps yield return obj; } - private readonly List prevNoteTimes = new List(max_notes_for_density); + private readonly LimitedCapacityQueue prevNoteTimes = new LimitedCapacityQueue(max_notes_for_density); private double density = int.MaxValue; private void computeDensity(double newNoteTime) { - if (prevNoteTimes.Count == max_notes_for_density) - prevNoteTimes.RemoveAt(0); - prevNoteTimes.Add(newNoteTime); + prevNoteTimes.Enqueue(newNoteTime); if (prevNoteTimes.Count >= 2) density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count; From 9d688733ac247169cc62fd29432e4ccd507f5440 Mon Sep 17 00:00:00 2001 From: tsrk Date: Thu, 15 Jun 2023 13:12:05 +0200 Subject: [PATCH 0211/2100] fix: correct key counter position in Triangles and Legacy skins --- osu.Game/Skinning/LegacySkin.cs | 2 +- osu.Game/Skinning/TrianglesSkin.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index e35f8fbe4d..e264af4c83 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -385,7 +385,7 @@ namespace osu.Game.Skinning { keyCounter.Anchor = Anchor.BottomRight; keyCounter.Origin = Anchor.BottomRight; - keyCounter.Position = new Vector2(10, -10 - hitError.Width); + keyCounter.Position = new Vector2(-10, -10 - hitError.Width); } } }) diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index 424c477bda..5f839fad0b 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -148,7 +148,7 @@ namespace osu.Game.Skinning { keyCounter.Anchor = Anchor.BottomRight; keyCounter.Origin = Anchor.BottomRight; - keyCounter.Position = new Vector2(10, -57 - 10); + keyCounter.Position = new Vector2(-10, -60 - 10); } }) { From d83bf029239bf54c4ef290b210c8d8dc232b206f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Jun 2023 17:50:46 +0900 Subject: [PATCH 0212/2100] Fix thing --- osu.Game/Database/RealmAccess.cs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 68a4679656..b2bbbf3155 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -945,22 +945,26 @@ namespace osu.Game.Database foreach (var score in scores) { - // Recalculate the old-style standardised score to see if this was an old lazer score. - bool oldScoreMatchesExpectations = StandardisedScoreMigrationTools.GetOldStandardised(score) == score.TotalScore; - // Some older scores don't have correct statistics populated, so let's give them benefit of doubt. - bool scoreIsVeryOld = score.Date < new DateTime(2023, 1, 1, 0, 0, 0); - - if (oldScoreMatchesExpectations || scoreIsVeryOld) + try { - try - { - long calculatedNew = StandardisedScoreMigrationTools.GetNewStandardised(score); - score.TotalScore = calculatedNew; - } - catch + // Recalculate the old-style standardised score to see if this was an old lazer score. + bool oldScoreMatchesExpectations = StandardisedScoreMigrationTools.GetOldStandardised(score) == score.TotalScore; + // Some older scores don't have correct statistics populated, so let's give them benefit of doubt. + bool scoreIsVeryOld = score.Date < new DateTime(2023, 1, 1, 0, 0, 0); + + if (oldScoreMatchesExpectations || scoreIsVeryOld) { + try + { + long calculatedNew = StandardisedScoreMigrationTools.GetNewStandardised(score); + score.TotalScore = calculatedNew; + } + catch + { + } } } + catch { } } break; From 51b5a0863f97b0d3e777fbcae5490e10848e0d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 15 Jun 2023 21:48:57 +0200 Subject: [PATCH 0213/2100] Apply migration to new standardised score on normal reimport too --- osu.Game/Database/RealmAccess.cs | 7 +------ .../Database/StandardisedScoreMigrationTools.cs | 14 ++++++++++++++ osu.Game/Scoring/ScoreImporter.cs | 5 +++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index b2bbbf3155..48eb2826f5 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -947,12 +947,7 @@ namespace osu.Game.Database { try { - // Recalculate the old-style standardised score to see if this was an old lazer score. - bool oldScoreMatchesExpectations = StandardisedScoreMigrationTools.GetOldStandardised(score) == score.TotalScore; - // Some older scores don't have correct statistics populated, so let's give them benefit of doubt. - bool scoreIsVeryOld = score.Date < new DateTime(2023, 1, 1, 0, 0, 0); - - if (oldScoreMatchesExpectations || scoreIsVeryOld) + if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(score)) { try { diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index af91bee9e4..66e64f3f7a 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.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.Linq; using osu.Game.Beatmaps; @@ -13,6 +14,19 @@ namespace osu.Game.Database { public static class StandardisedScoreMigrationTools { + public static bool ShouldMigrateToNewStandardised(ScoreInfo score) + { + if (score.IsLegacyScore) + return false; + + // Recalculate the old-style standardised score to see if this was an old lazer score. + bool oldScoreMatchesExpectations = GetOldStandardised(score) == score.TotalScore; + // Some older scores don't have correct statistics populated, so let's give them benefit of doubt. + bool scoreIsVeryOld = score.Date < new DateTime(2023, 1, 1, 0, 0, 0); + + return oldScoreMatchesExpectations || scoreIsVeryOld; + } + public static long GetNewStandardised(ScoreInfo score) { int maxJudgementIndex = 0; diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 1c24cfbc85..16658a598a 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -83,6 +83,11 @@ namespace osu.Game.Scoring if (string.IsNullOrEmpty(model.MaximumStatisticsJson)) model.MaximumStatisticsJson = JsonConvert.SerializeObject(model.MaximumStatistics); + + // for pre-ScoreV2 lazer scores, apply a best-effort conversion of total score to ScoreV2. + // this requires: max combo, statistics, max statistics (where available), and mods to already be populated on the score. + if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model)) + model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model); } /// From 94b7de4b3f5b131a231aba17c48c75c721884169 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Jun 2023 14:01:56 +0900 Subject: [PATCH 0214/2100] Fix old-new standardised score conversion missing some scores due to not rounding correctly --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 66e64f3f7a..582a656efa 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -182,7 +182,7 @@ namespace osu.Game.Database foreach (var mod in score.Mods) modMultiplier *= mod.ScoreMultiplier; - return (long)((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier); + return (long)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier); } private class FakeHit : HitObject From 1f17f416a4e43112303407998bf09b6b9fe42e32 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Jun 2023 14:04:18 +0900 Subject: [PATCH 0215/2100] Force migration of old-new standardised scores to run once more --- osu.Game/Database/RealmAccess.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 48eb2826f5..da4caa42ba 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -77,8 +77,9 @@ namespace osu.Game.Database /// 27 2023-06-06 Added EditorTimestamp to BeatmapInfo. /// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files. /// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes. + /// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations. /// - private const int schema_version = 29; + private const int schema_version = 30; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -938,6 +939,7 @@ namespace osu.Game.Database } case 29: + case 30: { var scores = migration.NewRealm .All() From b5de109cb31ab021778677722c959e49d6c6f4ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Jun 2023 14:54:19 +0900 Subject: [PATCH 0216/2100] Fix osu!mania hold notes sometimes looking incorrect after rewind --- osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 3f91328128..faeb133615 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -254,6 +254,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y; sizingContainer.Height = 1 - yOffset / DrawHeight; } + else + sizingContainer.Height = 1; } protected override void CheckForResult(bool userTriggered, double timeOffset) From ce41ef6e5d3e6163dfbd6d1dfadb53aabfe409d8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Jun 2023 15:24:30 +0900 Subject: [PATCH 0217/2100] Move `OrderByTotalScore()` to an extension method --- .../Overlays/BeatmapSet/Scores/ScoresContainer.cs | 5 +---- osu.Game/Scoring/ScoreInfoExtensions.cs | 13 +++++++++++++ osu.Game/Scoring/ScoreManager.cs | 11 ----------- .../OnlinePlay/Playlists/PlaylistsResultsScreen.cs | 2 +- osu.Game/Screens/Select/Carousel/TopLocalRank.cs | 5 +---- .../Select/Leaderboards/BeatmapLeaderboard.cs | 7 ++----- 6 files changed, 18 insertions(+), 25 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index 6d89313979..b53b7826f3 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -47,9 +47,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores [Resolved] private RulesetStore rulesets { get; set; } - [Resolved] - private ScoreManager scoreManager { get; set; } - private GetScoresRequest getScoresRequest; private CancellationTokenSource loadCancellationSource; @@ -85,7 +82,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores MD5Hash = apiBeatmap.MD5Hash }; - var scores = scoreManager.OrderByTotalScore(value.Scores.Select(s => s.ToScoreInfo(rulesets, beatmapInfo))).ToArray(); + var scores = value.Scores.Select(s => s.ToScoreInfo(rulesets, beatmapInfo)).OrderByTotalScore().ToArray(); var topScore = scores.First(); scoreTable.DisplayScores(scores, apiBeatmap.Status.GrantsPerformancePoints()); diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs index 7979ca8aaa..e15ab0d34c 100644 --- a/osu.Game/Scoring/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/ScoreInfoExtensions.cs @@ -3,6 +3,8 @@ #nullable disable +using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; namespace osu.Game.Scoring @@ -13,5 +15,16 @@ namespace osu.Game.Scoring /// A user-presentable display title representing this score. /// public static string GetDisplayTitle(this IScoreInfo scoreInfo) => $"{scoreInfo.User.Username} playing {scoreInfo.Beatmap.GetDisplayTitle()}"; + + /// + /// Orders an array of s by total score. + /// + /// The array of s to reorder. + /// The given ordered by decreasing total score. + public static IEnumerable OrderByTotalScore(this IEnumerable scores) + => scores.OrderByDescending(s => s.TotalScore) + .ThenBy(s => s.OnlineID) + // Local scores may not have an online ID. Fall back to date in these cases. + .ThenBy(s => s.Date); } } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index d5509538fd..9ba7339a31 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -69,17 +69,6 @@ namespace osu.Game.Scoring return Realm.Run(r => r.All().FirstOrDefault(query)?.Detach()); } - /// - /// Orders an array of s by total score. - /// - /// The array of s to reorder. - /// The given ordered by decreasing total score. - public IEnumerable OrderByTotalScore(IEnumerable scores) - => scores.OrderByDescending(s => s.TotalScore) - .ThenBy(s => s.OnlineID) - // Local scores may not have an online ID. Fall back to date in these cases. - .ThenBy(s => s.Date); - /// /// Retrieves a bindable that represents the total score of a . /// diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs index d40d43cd54..aa72394ac9 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs @@ -182,7 +182,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// An optional pivot around which the scores were retrieved. private void performSuccessCallback([NotNull] Action> callback, [NotNull] List scores, [CanBeNull] MultiplayerScores pivot = null) => Schedule(() => { - var scoreInfos = scoreManager.OrderByTotalScore(scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, Beatmap.Value.BeatmapInfo))).ToArray(); + var scoreInfos = scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); // Select a score if we don't already have one selected. // Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll). diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index a57a8b0f27..7c632b63db 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -29,9 +29,6 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private RealmAccess realm { get; set; } = null!; - [Resolved] - private ScoreManager scoreManager { get; set; } = null!; - [Resolved] private IAPIProvider api { get; set; } = null!; @@ -78,7 +75,7 @@ namespace osu.Game.Screens.Select.Carousel if (changes?.HasCollectionChanges() == false) return; - ScoreInfo? topScore = scoreManager.OrderByTotalScore(sender.Detach()).FirstOrDefault(); + ScoreInfo? topScore = sender.Detach().OrderByTotalScore().FirstOrDefault(); updateable.Rank = topScore?.Rank; updateable.Alpha = topScore != null ? 1 : 0; diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 2b40b9faf8..4c41ed3622 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -67,9 +67,6 @@ namespace osu.Game.Screens.Select.Leaderboards } } - [Resolved] - private ScoreManager scoreManager { get; set; } = null!; - [Resolved] private IBindable ruleset { get; set; } = null!; @@ -164,7 +161,7 @@ namespace osu.Game.Screens.Select.Leaderboards return; SetScores( - scoreManager.OrderByTotalScore(response.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo))), + response.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo)).OrderByTotalScore(), response.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo) ); }); @@ -222,7 +219,7 @@ namespace osu.Game.Screens.Select.Leaderboards scores = scores.Where(s => selectedMods.SetEquals(s.Mods.Select(m => m.Acronym))); } - scores = scoreManager.OrderByTotalScore(scores.Detach()); + scores = scores.Detach().OrderByTotalScore(); SetScores(scores); } From 362aa4b3763223c3294b23029eb6284d45525ad6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Jun 2023 15:26:13 +0900 Subject: [PATCH 0218/2100] Also move `GetMaxAchievableCombo` --- osu.Game/Scoring/ScoreInfoExtensions.cs | 10 ++++++++-- osu.Game/Scoring/ScoreManager.cs | 7 ------- .../Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs index e15ab0d34c..85598076d6 100644 --- a/osu.Game/Scoring/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/ScoreInfoExtensions.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. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Scoring { @@ -26,5 +25,12 @@ namespace osu.Game.Scoring .ThenBy(s => s.OnlineID) // Local scores may not have an online ID. Fall back to date in these cases. .ThenBy(s => s.Date); + + /// + /// Retrieves the maximum achievable combo for the provided score. + /// + /// The to compute the maximum achievable combo for. + /// The maximum achievable combo. + public static int GetMaximumAchievableCombo(this ScoreInfo score) => score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value); } } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 9ba7339a31..55bcb9f79d 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -89,13 +89,6 @@ namespace osu.Game.Scoring /// The bindable containing the formatted total score string. public Bindable GetBindableTotalScoreString([NotNull] ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score)); - /// - /// Retrieves the maximum achievable combo for the provided score. - /// - /// The to compute the maximum achievable combo for. - /// The maximum achievable combo. - public int GetMaximumAchievableCombo([NotNull] ScoreInfo score) => score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value); - /// /// Provides the total score of a . Responds to changes in the currently-selected . /// diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index fe74c1ba0d..82c429798e 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -73,7 +73,7 @@ namespace osu.Game.Screens.Ranking.Expanded var topStatistics = new List { new AccuracyStatistic(score.Accuracy), - new ComboStatistic(score.MaxCombo, scoreManager.GetMaximumAchievableCombo(score)), + new ComboStatistic(score.MaxCombo, score.GetMaximumAchievableCombo()), new PerformanceStatistic(score), }; From 36954e55ad395b99673d15ee68eba42800fa8362 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Jun 2023 17:15:45 +0900 Subject: [PATCH 0219/2100] Fix incorrect mapping when distance spacing is not 1.0x --- .../Compose/Components/CircularDistanceSnapGrid.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index 2eec833832..63886e38eb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -101,12 +101,11 @@ namespace osu.Game.Screens.Edit.Compose.Components if (travelLength < DistanceBetweenTicks) travelLength = DistanceBetweenTicks; - if (LimitedDistanceSnap.Value) - travelLength = SnapProvider.DurationToDistance(ReferenceObject, editorClock.CurrentTime - ReferenceObject.GetEndTime()); - - // When interacting with the resolved snap provider, the distance spacing multiplier should first be removed - // to allow for snapping at a non-multiplied ratio. - float snappedDistance = SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier); + float snappedDistance = LimitedDistanceSnap.Value + ? SnapProvider.DurationToDistance(ReferenceObject, editorClock.CurrentTime - ReferenceObject.GetEndTime()) + // When interacting with the resolved snap provider, the distance spacing multiplier should first be removed + // to allow for snapping at a non-multiplied ratio. + : SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier); double snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance); From d965d39c4442849561d59c25186c2a6fb6ad7af9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Jun 2023 17:16:11 +0900 Subject: [PATCH 0220/2100] Fix distance snap grid circles not correctly being centered on snap point --- .../Edit/Compose/Components/CircularDistanceSnapGrid.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index d6e4e1f030..f27ecf60cc 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -63,13 +63,14 @@ namespace osu.Game.Screens.Edit.Compose.Components for (int i = 0; i < requiredCircles; i++) { float diameter = (offset + (i + 1) * DistanceBetweenTicks) * 2; + const float thickness = 4; AddInternal(new Ring(ReferenceObject, GetColourForIndexFromPlacement(i)) { Position = StartPosition, Origin = Anchor.Centre, - Size = new Vector2(diameter), - InnerRadius = 4 * 1f / diameter, + Size = new Vector2(diameter + thickness / 2), + InnerRadius = thickness * 1f / diameter, }); } } From 28696f595fbd601b18808af1c5dba47c7eb764ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 16 Jun 2023 16:24:07 +0200 Subject: [PATCH 0221/2100] Privatise setter --- osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 9882f6596f..8aa2fa9f45 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -65,7 +65,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// When enabled, distance snap should only snap to the current time (as per the editor clock). /// This is to emulate stable behaviour. /// - protected Bindable LimitedDistanceSnap; + protected Bindable LimitedDistanceSnap { get; private set; } [BackgroundDependencyLoader] private void load(OsuConfigManager config) From f9321a24d9192ab49980eaa657bfbc2a412a051d Mon Sep 17 00:00:00 2001 From: tsrk Date: Fri, 16 Jun 2023 17:24:40 +0200 Subject: [PATCH 0222/2100] test: change hideTarget drawable and testing logic Doesn't change what it needs to test conceptually --- .../Visual/Gameplay/TestSceneHUDOverlay.cs | 20 +++++++++---------- .../TestSceneSkinEditorMultipleSkins.cs | 1 - .../Gameplay/TestSceneSkinnableHUDOverlay.cs | 5 +++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index bbb10c5957..5002281544 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock()); // best way to check without exposing. - private Drawable hideTarget => hudOverlay.ChildrenOfType().First(); + private Drawable hideTarget => hudOverlay.ChildrenOfType().First(); private Drawable keyCounterFlow => hudOverlay.ChildrenOfType().First().ChildrenOfType>().Single(); [BackgroundDependencyLoader] @@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("showhud is set", () => hudOverlay.ShowHud.Value); - AddAssert("hidetarget is visible", () => hideTarget.IsPresent); + AddAssert("hidetarget is visible", () => hideTarget.Alpha, () => Is.GreaterThan(0)); AddAssert("key counter flow is visible", () => keyCounterFlow.IsPresent); AddAssert("pause button is visible", () => hudOverlay.HoldToQuit.IsPresent); } @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); - AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); + AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent); // Key counter flow container should not be affected by this, only the key counter display will be hidden as checked above. @@ -109,13 +109,13 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set hud to never show", () => localConfig.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never)); - AddUntilStep("wait for fade", () => !hideTarget.IsPresent); + AddUntilStep("wait for fade", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); AddStep("trigger momentary show", () => InputManager.PressKey(Key.ControlLeft)); - AddUntilStep("wait for visible", () => hideTarget.IsPresent); + AddUntilStep("wait for visible", () => hideTarget.Alpha, () => Is.GreaterThan(0)); AddStep("stop trigering", () => InputManager.ReleaseKey(Key.ControlLeft)); - AddUntilStep("wait for fade", () => !hideTarget.IsPresent); + AddUntilStep("wait for fade", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); } [Test] @@ -144,11 +144,11 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); - AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); + AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); AddAssert("key counters hidden", () => !keyCounterFlow.IsPresent); AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true); - AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent); + AddUntilStep("hidetarget is visible", () => hideTarget.Alpha, () => Is.GreaterThan(0)); AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent); } @@ -171,7 +171,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); - AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); + AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); AddStep("attempt activate", () => { @@ -211,7 +211,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); - AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); + AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); AddStep("attempt seek", () => { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index ac772b980e..4ae115a68d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -59,7 +59,6 @@ namespace osu.Game.Tests.Visual.Gameplay // Add any key just to display the key counter visually. hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); - scoreProcessor.Combo.Value = 1; return new Container diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index 6532aa4ae5..cab52ddab5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; using osu.Game.Tests.Gameplay; using osuTK.Input; @@ -43,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay private IEnumerable hudOverlays => CreatedDrawables.OfType(); // best way to check without exposing. - private Drawable hideTarget => hudOverlay.ChildrenOfType().First(); + private Drawable hideTarget => hudOverlay.ChildrenOfType().First(); private Drawable keyCounterFlow => hudOverlay.ChildrenOfType().First().ChildrenOfType>().Single(); [Test] @@ -73,7 +74,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false)); - AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); + AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent); // Key counter flow container should not be affected by this, only the key counter display will be hidden as checked above. From b9d6ba193422afe8ed72f62a43b5085885157b83 Mon Sep 17 00:00:00 2001 From: tsrk Date: Fri, 16 Jun 2023 18:26:30 +0200 Subject: [PATCH 0223/2100] test: add skin deserialisation test resource --- .../Archives/modified-argon-pro-20230616.osk | Bin 0 -> 1234 bytes osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 osu.Game.Tests/Resources/Archives/modified-argon-pro-20230616.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-pro-20230616.osk b/osu.Game.Tests/Resources/Archives/modified-argon-pro-20230616.osk new file mode 100644 index 0000000000000000000000000000000000000000..5e5d7960dd585f9ed89c16e4b961e12fe968ace3 GIT binary patch literal 1234 zcmWIWW@Zs#U|`^2V3>R%?D;~^O|yZ#?GO=$;_S>kz0AB!)iZ{CO%4KX59hU5{|RzT zIudXx@@8aast#LOL+@_mlt?Fw*{$xMxRTb~pZ>ja?`fgyjTcuI?}~VNah1!oj#jk` zGFOAQ=vr=X>Pc))YxVogm&RwG_jiTjyNegLxQdos+c+V8%KnuP8O~4SakW}<^6!~L zjdzrN^=7a9zq-e1{iH;mu02*^p^bM|Z=9PNyyRqNUvrz3d1aU0B(?Ye6b~r1KeVm_ zI({+`^8zu*1DSbg`FdH!`FWzmv5J$tr%PSS)F z!{~CK4eDkWl6;$%OuBL*X~WwabN36D&lU;jc{fA)dPZf&i}O2m?#ZJ%(z=XQEtiRb z!4(+rVn9dwCT8Y>|EA=N9DWrRJ3sgI)VJJU0K9fk^HAE&l~R@`U;P2vGZYAVK=F z3GdW0{ZLWMhe9?zz3XIet$0;GpLyW~-P(n^OKv@2om>22?~flbkA8oDFSFdtrR=m5 zWBgN}t9BOOB{LpIteC*b9h1+Pemv*K=@Qmyt2O6N(9hva5mIcplA;sguPI%mPj+P}}SYNeP$wUOYW zF#C+eH74d|owesJ3)2N}En4z&SKjKl1wr1GxsTo*Y(K23KPB| zDtUjUbnfeU#%1jJUEp7%&9lhZ{{nk2EwKM=J;Sap`&dhxQ*r{&^o{4Lv_+)O?3|&L zk{x&d&4;~@G>p|JI$7+UnRipHI&W&O&Y{rH8|7U&H)+`Jo!Vqz#J=AL zr@Wq8s`fk3-{h;*v;GE7WBI)7`PN6Ta%_x$RblF0D4(pV?sY6V#IWhw^KBa(Wx&-KJdY)tX=bly$y-%lGl^ z>d?j?m(qUL7i^xyC|CHIGw@;ghIv_^CMGgkmd{yjI5D$w#>?*3pW#NmiJX^Mn!GvA z%yH+=T)t9t+rKxKK~Aa?Ej-Pp*KM9O%{H0O==wCxMd#|RX*@TiCL9e|)t!^rl5nzs zQ6gh<-nM=**QQp5Z%;Jk1plqveAD7vLDky-?Vowti+L=9ccd6KZG3d;Mb{k{YuQI> z=Ck}npFNA-(>L$4;BrUX-xDss3;OlF&OBqYr_HPK)eqKOx9r}0o8NrrhP2DaZ!SKx ziGM$1fHxzP2m|hd0GLW4pbE-AXFDTFQRKj&l3o(X^gmXNPssh Q8%P~95N-w1>?|N200~+DYybcN literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index bd8088cfb6..d60dd3da1c 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -51,6 +51,8 @@ namespace osu.Game.Tests.Skins "Archives/modified-default-20230117.osk", // Covers player avatar and flag. "Archives/modified-argon-20230305.osk", + // Covers key counters + "Archives/modified-argon-pro-20230616.osk" }; /// From a62b11606e75b579fa2086b5f191c251f54b41f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 17 Jun 2023 01:32:41 +0900 Subject: [PATCH 0224/2100] Attempt to fix NaN fps display The only thing I can see which could cause this is reading from the `drawClock.ElapsedFrameTime` after the `isSpike` read causing a div-by-zero. Reading the values once at the start should avoid this. --- osu.Game/Graphics/UserInterface/FPSCounter.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/FPSCounter.cs b/osu.Game/Graphics/UserInterface/FPSCounter.cs index 9dbeba6449..c1ef573848 100644 --- a/osu.Game/Graphics/UserInterface/FPSCounter.cs +++ b/osu.Game/Graphics/UserInterface/FPSCounter.cs @@ -167,9 +167,12 @@ namespace osu.Game.Graphics.UserInterface { base.Update(); + double elapsedDrawFrameTime = drawClock.ElapsedFrameTime; + double elapsedUpdateFrameTime = updateClock.ElapsedFrameTime; + // If the game goes into a suspended state (ie. debugger attached or backgrounded on a mobile device) // we want to ignore really long periods of no processing. - if (updateClock.ElapsedFrameTime > 10000) + if (elapsedUpdateFrameTime > 10000) return; mainContent.Width = Math.Max(mainContent.Width, counters.DrawWidth); @@ -178,17 +181,17 @@ namespace osu.Game.Graphics.UserInterface // frame limiter (we want to show the FPS as it's changing, even if it isn't an outlier). bool aimRatesChanged = updateAimFPS(); - bool hasUpdateSpike = displayedFrameTime < spike_time_ms && updateClock.ElapsedFrameTime > spike_time_ms; + bool hasUpdateSpike = displayedFrameTime < spike_time_ms && elapsedUpdateFrameTime > spike_time_ms; // use elapsed frame time rather then FramesPerSecond to better catch stutter frames. - bool hasDrawSpike = displayedFpsCount > (1000 / spike_time_ms) && drawClock.ElapsedFrameTime > spike_time_ms; + bool hasDrawSpike = displayedFpsCount > (1000 / spike_time_ms) && elapsedDrawFrameTime > spike_time_ms; const float damp_time = 100; - displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, updateClock.ElapsedFrameTime, hasUpdateSpike ? 0 : damp_time, updateClock.ElapsedFrameTime); + displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, elapsedUpdateFrameTime, hasUpdateSpike ? 0 : damp_time, elapsedUpdateFrameTime); if (hasDrawSpike) // show spike time using raw elapsed value, to account for `FramesPerSecond` being so averaged spike frames don't show. - displayedFpsCount = 1000 / drawClock.ElapsedFrameTime; + displayedFpsCount = 1000 / elapsedDrawFrameTime; else displayedFpsCount = Interpolation.DampContinuously(displayedFpsCount, drawClock.FramesPerSecond, damp_time, Time.Elapsed); From 3b1f92d8b84f081738d7b660aaa03910ac8fcecc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 17 Jun 2023 01:37:09 +0900 Subject: [PATCH 0225/2100] Fix fix logic causing further regression on release --- .../Objects/Drawables/DrawableHoldNote.cs | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index faeb133615..a8563d65c4 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -242,17 +242,20 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2; bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2; - // As the note is being held, adjust the size of the sizing container. This has two effects: - // 1. The contained masking container will mask the body and ticks. - // 2. The head note will move along with the new "head position" in the container. - // - // As per stable, this should not apply for early hits, waiting until the object starts to touch the - // judgement area first. - if (Head.IsHit && releaseTime == null && DrawHeight > 0 && Time.Current >= HitObject.StartTime) + if (Time.Current >= HitObject.StartTime) { - // How far past the hit target this hold note is. - float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y; - sizingContainer.Height = 1 - yOffset / DrawHeight; + // As the note is being held, adjust the size of the sizing container. This has two effects: + // 1. The contained masking container will mask the body and ticks. + // 2. The head note will move along with the new "head position" in the container. + // + // As per stable, this should not apply for early hits, waiting until the object starts to touch the + // judgement area first. + if (Head.IsHit && releaseTime == null && DrawHeight > 0) + { + // How far past the hit target this hold note is. + float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y; + sizingContainer.Height = 1 - yOffset / DrawHeight; + } } else sizingContainer.Height = 1; From b960741ff78a2926d334f81bcf042c83b26130d3 Mon Sep 17 00:00:00 2001 From: tsrk Date: Fri, 16 Jun 2023 18:54:19 +0200 Subject: [PATCH 0226/2100] test: adapt touch input test to changes --- .../TestSceneOsuTouchInput.cs | 58 ++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs index bb424eb587..4572970011 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.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.Diagnostics; using System.Linq; using NUnit.Framework; @@ -38,6 +39,8 @@ namespace osu.Game.Rulesets.Osu.Tests private DefaultKeyCounter rightKeyCounter = null!; + private KeyCounterController controller = null!; + private OsuInputManager osuInputManager = null!; private Container mainContent = null!; @@ -53,35 +56,50 @@ namespace osu.Game.Rulesets.Osu.Tests { osuInputManager = new OsuInputManager(new OsuRuleset().RulesetInfo) { - Child = mainContent = new Container + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] + controller = new KeyCounterController(), + mainContent = new DependencyProvidingContainer { - leftKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.LeftButton)) + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + CachedDependencies = new (Type, object)[] { (typeof(KeyCounterController), controller) }, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.CentreRight, - Depth = float.MinValue, - X = -100, + new OsuCursorContainer + { + Depth = float.MinValue, + } }, - rightKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.RightButton)) - { - Anchor = Anchor.Centre, - Origin = Anchor.CentreLeft, - Depth = float.MinValue, - X = 100, - }, - new OsuCursorContainer - { - Depth = float.MinValue, - } }, } }, new TouchVisualiser(), }; + + InputTrigger triggerLeft; + InputTrigger triggerRight; + + controller.Add(triggerLeft = new TestActionKeyCounterTrigger(OsuAction.LeftButton)); + controller.Add(triggerRight = new TestActionKeyCounterTrigger(OsuAction.RightButton)); + + mainContent.AddRange(new[] + { + leftKeyCounter = new DefaultKeyCounter(triggerLeft) + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + Depth = float.MinValue, + X = -100, + }, + rightKeyCounter = new DefaultKeyCounter(triggerRight) + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + Depth = float.MinValue, + X = 100, + }, + }); }); } From 61101335cca6968ad37092230663932b660ce2d4 Mon Sep 17 00:00:00 2001 From: tsrk Date: Fri, 16 Jun 2023 19:00:09 +0200 Subject: [PATCH 0227/2100] test: fix `KeyCounterController` not provided as a dependency --- .../Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index 514a2d7e84..56c405d81f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osu.Game.Storyboards; @@ -77,7 +78,8 @@ namespace osu.Game.Tests.Visual.Gameplay (typeof(ScoreProcessor), actualComponentsContainer.Dependencies.Get()), (typeof(HealthProcessor), actualComponentsContainer.Dependencies.Get()), (typeof(GameplayState), actualComponentsContainer.Dependencies.Get()), - (typeof(IGameplayClock), actualComponentsContainer.Dependencies.Get()) + (typeof(IGameplayClock), actualComponentsContainer.Dependencies.Get()), + (typeof(KeyCounterController), actualComponentsContainer.Dependencies.Get()) }, }; From f70342bd6772cefbfcd389dc30b780116c6ed8d3 Mon Sep 17 00:00:00 2001 From: Maksim Kan Date: Tue, 13 Jun 2023 18:18:46 +0300 Subject: [PATCH 0228/2100] Fix Triangle skin colors with Dual Stage mod --- .../Skinning/Default/ManiaTrianglesSkinTransformer.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs index eb51179cea..3e0fe8ed4b 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs @@ -35,10 +35,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default var stage = beatmap.GetStageForColumnIndex(column); - if (stage.IsSpecialColumn(column)) + int columnInStage = column % stage.Columns; + + if (stage.IsSpecialColumn(columnInStage)) return SkinUtils.As(new Bindable(colourSpecial)); - int distanceToEdge = Math.Min(column, (stage.Columns - 1) - column); + int distanceToEdge = Math.Min(columnInStage, (stage.Columns - 1) - columnInStage); return SkinUtils.As(new Bindable(distanceToEdge % 2 == 0 ? colourOdd : colourEven)); } } From 688e65475dd938544924ba2d7e06355b50fb4bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 17 Jun 2023 10:25:09 +0200 Subject: [PATCH 0229/2100] Add better test coverage of dual stages in skinnable tests --- .../Skinning/TestScenePlayfield.cs | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs index f85e303940..6485cbb76b 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; +using osuTK; namespace osu.Game.Rulesets.Mania.Tests.Skinning { @@ -25,22 +26,35 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning new StageDefinition(2) }; - SetContents(_ => new ManiaPlayfield(stageDefinitions)); + SetContents(_ => new ManiaInputManager(new ManiaRuleset().RulesetInfo, 2) + { + Child = new ManiaPlayfield(stageDefinitions) + }); }); } - [Test] - public void TestDualStages() + [TestCase(2)] + [TestCase(3)] + [TestCase(5)] + public void TestDualStages(int columnCount) { AddStep("create stage", () => { stageDefinitions = new List { - new StageDefinition(2), - new StageDefinition(2) + new StageDefinition(columnCount), + new StageDefinition(columnCount) }; - SetContents(_ => new ManiaPlayfield(stageDefinitions)); + SetContents(_ => new ManiaInputManager(new ManiaRuleset().RulesetInfo, (int)PlayfieldType.Dual + 2 * columnCount) + { + Child = new ManiaPlayfield(stageDefinitions) + { + // bit of a hack to make sure the dual stages fit on screen without overlapping each other. + Size = new Vector2(1.5f), + Scale = new Vector2(1 / 1.5f) + } + }); }); } From 4919069ea6d115c49b0b7993016d92110e350974 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 18 Jun 2023 01:58:02 +0900 Subject: [PATCH 0230/2100] Avoid humanizer regex compilation overhead when opening song select for the first time --- osu.Game/Screens/Select/SongSelect.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 4d6a5398c5..91799dabf0 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Humanizer; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -864,7 +863,7 @@ namespace osu.Game.Screens.Select { // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 // but also in this case we want support for formatting a number within a string). - FilterControl.InformationalText = $"{"match".ToQuantity(Carousel.CountDisplayed, "#,0")}"; + FilterControl.InformationalText = Carousel.CountDisplayed != 1 ? $"{Carousel.CountDisplayed} matches" : $"{Carousel.CountDisplayed} match"; } private bool boundLocalBindables; From 25fa4a2eb53b00fcb66a6fb2ce4f5fc5dc7d8082 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 17 Jun 2023 19:57:08 +0300 Subject: [PATCH 0231/2100] Move `DragDrop` handling to base game implementation for iOS support --- osu.Desktop/OsuGameDesktop.cs | 45 -------------------------------- osu.Game/OsuGame.cs | 48 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 45 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index d92fea27bf..21cea3ba76 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -2,12 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.Versioning; -using System.Threading.Tasks; using Microsoft.Win32; using osu.Desktop.Security; using osu.Framework.Platform; @@ -17,7 +15,6 @@ using osu.Framework; using osu.Framework.Logging; using osu.Game.Updater; using osu.Desktop.Windows; -using osu.Framework.Threading; using osu.Game.IO; using osu.Game.IPC; using osu.Game.Utils; @@ -138,52 +135,10 @@ namespace osu.Desktop desktopWindow.CursorState |= CursorState.Hidden; desktopWindow.Title = Name; - desktopWindow.DragDrop += f => - { - // on macOS, URL associations are handled via SDL_DROPFILE events. - if (f.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) - { - HandleLink(f); - return; - } - - fileDrop(new[] { f }); - }; } protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo(); - private readonly List importableFiles = new List(); - private ScheduledDelegate? importSchedule; - - private void fileDrop(string[] filePaths) - { - lock (importableFiles) - { - importableFiles.AddRange(filePaths); - - Logger.Log($"Adding {filePaths.Length} files for import"); - - // File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms. - // In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch. - importSchedule?.Cancel(); - importSchedule = Scheduler.AddDelayed(handlePendingImports, 100); - } - } - - private void handlePendingImports() - { - lock (importableFiles) - { - Logger.Log($"Handling batch import of {importableFiles.Count} files"); - - string[] paths = importableFiles.ToArray(); - importableFiles.Clear(); - - Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning); - } - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 3768dad370..a80639d4ff 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -28,6 +29,7 @@ using osu.Framework.Input.Events; using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Localisation; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; @@ -281,6 +283,52 @@ namespace osu.Game protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + private readonly List dragDropFiles = new List(); + private ScheduledDelegate dragDropImportSchedule; + + public override void SetHost(GameHost host) + { + base.SetHost(host); + + if (host.Window is SDL2Window sdlWindow) + { + sdlWindow.DragDrop += path => + { + // on macOS/iOS, URL associations are handled via SDL_DROPFILE events. + if (path.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) + { + HandleLink(path); + return; + } + + lock (dragDropFiles) + { + dragDropFiles.Add(path); + + Logger.Log($@"Adding ""{Path.GetFileName(path)}"" for import"); + + // File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms. + // In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch. + dragDropImportSchedule?.Cancel(); + dragDropImportSchedule = Scheduler.AddDelayed(handlePendingDragDropImports, 100); + } + }; + } + } + + private void handlePendingDragDropImports() + { + lock (dragDropFiles) + { + Logger.Log($"Handling batch import of {dragDropFiles.Count} files"); + + string[] paths = dragDropFiles.ToArray(); + dragDropFiles.Clear(); + + Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning); + } + } + [BackgroundDependencyLoader] private void load() { From eafd774044faad4030a4f4d19a4c26d03e13f2f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 17 Jun 2023 20:03:24 +0200 Subject: [PATCH 0232/2100] Bring back old formatting spec --- osu.Game/Screens/Select/SongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 91799dabf0..47e5325baf 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -863,7 +863,7 @@ namespace osu.Game.Screens.Select { // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 // but also in this case we want support for formatting a number within a string). - FilterControl.InformationalText = Carousel.CountDisplayed != 1 ? $"{Carousel.CountDisplayed} matches" : $"{Carousel.CountDisplayed} match"; + FilterControl.InformationalText = Carousel.CountDisplayed != 1 ? $"{Carousel.CountDisplayed:#,0} matches" : $"{Carousel.CountDisplayed:#,0} match"; } private bool boundLocalBindables; From fdebf93ae41a87010b973a4ec73bbf9312e8b99b Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 17 Jun 2023 14:55:27 -0700 Subject: [PATCH 0233/2100] Fix incorrect xmldoc of `IBeatmapInfo.Length` --- osu.Game/Beatmaps/IBeatmapInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs index 4f2c08f63d..b8c69cc525 100644 --- a/osu.Game/Beatmaps/IBeatmapInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapInfo.cs @@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps IBeatmapSetInfo? BeatmapSet { get; } /// - /// The playable length in milliseconds of this beatmap. + /// The total length in milliseconds of this beatmap. /// double Length { get; } From 9ae864c219c1bed9f6777383f9ddcbf13fca4019 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 17 Jun 2023 15:00:32 -0700 Subject: [PATCH 0234/2100] Fix beatmap info length tooltip not showing actual drain length --- .../Visual/Online/TestSceneBeatmapSetOverlay.cs | 1 + osu.Game/Beatmaps/IBeatmapOnlineInfo.cs | 5 +++++ osu.Game/Online/API/Requests/Responses/APIBeatmap.cs | 10 ++++++++++ osu.Game/Overlays/BeatmapSet/BasicStats.cs | 8 ++++---- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index a27c4ddad2..d9763ef6c8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -86,6 +86,7 @@ namespace osu.Game.Tests.Visual.Online StarRating = 9.99, DifficultyName = @"TEST", Length = 456000, + HitLength = 400000, RulesetID = 3, CircleSize = 1, DrainRate = 2.3f, diff --git a/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs b/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs index e1634e7d24..707a0696ba 100644 --- a/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs @@ -59,5 +59,10 @@ namespace osu.Game.Beatmaps int PassCount { get; } APIFailTimes? FailTimes { get; } + + /// + /// The playable length in milliseconds of this beatmap. + /// + double HitLength { get; } } } diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index 7d6740ee46..902b651be9 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -63,6 +63,16 @@ namespace osu.Game.Online.API.Requests.Responses set => Length = TimeSpan.FromSeconds(value).TotalMilliseconds; } + [JsonIgnore] + public double HitLength { get; set; } + + [JsonProperty(@"hit_length")] + private double hitLengthInSeconds + { + get => TimeSpan.FromMilliseconds(HitLength).TotalSeconds; + set => HitLength = TimeSpan.FromSeconds(value).TotalMilliseconds; + } + [JsonProperty(@"convert")] public bool Convert { get; set; } diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs index 4a9a3d8089..3cc655d561 100644 --- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs +++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs @@ -68,13 +68,13 @@ namespace osu.Game.Overlays.BeatmapSet } else { - length.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(TimeSpan.FromMilliseconds(beatmapInfo.Length).ToFormattedDuration()); length.Value = TimeSpan.FromMilliseconds(beatmapInfo.Length).ToFormattedDuration(); - var onlineInfo = beatmapInfo as IBeatmapOnlineInfo; + if (beatmapInfo is not IBeatmapOnlineInfo onlineInfo) return; - circleCount.Value = (onlineInfo?.CircleCount ?? 0).ToLocalisableString(@"N0"); - sliderCount.Value = (onlineInfo?.SliderCount ?? 0).ToLocalisableString(@"N0"); + circleCount.Value = onlineInfo.CircleCount.ToLocalisableString(@"N0"); + sliderCount.Value = onlineInfo.SliderCount.ToLocalisableString(@"N0"); + length.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(TimeSpan.FromMilliseconds(onlineInfo.HitLength).ToFormattedDuration()); } } From eb31fdecee97c8551788aa6422aac492357230fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jun 2023 13:48:56 +0900 Subject: [PATCH 0235/2100] Apply osu! side changes in line with `FocusedOverlayContainer.PopIn` `abstract` change See https://github.com/ppy/osu-framework/pull/5834 --- .../Visual/UserInterface/TestSceneOverlayContainer.cs | 4 ++++ osu.Game/Collections/ManageCollectionsDialog.cs | 2 -- osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs | 1 - osu.Game/Overlays/AccountCreationOverlay.cs | 1 - osu.Game/Overlays/ChatOverlay.cs | 2 -- osu.Game/Overlays/DialogOverlay.cs | 1 - osu.Game/Overlays/LoginOverlay.cs | 2 -- osu.Game/Overlays/MedalOverlay.cs | 6 +++++- osu.Game/Overlays/Mods/ShearedOverlayContainer.cs | 1 - osu.Game/Overlays/NotificationOverlay.cs | 2 -- osu.Game/Overlays/NowPlayingOverlay.cs | 2 -- osu.Game/Overlays/SettingsPanel.cs | 2 -- osu.Game/Overlays/WaveOverlayContainer.cs | 2 -- .../OnlinePlay/Match/Components/RoomSettingsOverlay.cs | 2 -- osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs | 2 -- 15 files changed, 9 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs index d9c2774611..bb94912c83 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs @@ -101,6 +101,10 @@ namespace osu.Game.Tests.Visual.UserInterface }, }; } + + protected override void PopIn() + { + } } } } diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index 36142cf26f..31016b807b 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -114,8 +114,6 @@ namespace osu.Game.Collections protected override void PopIn() { - base.PopIn(); - lowPassFilter.CutoffTo(300, 100, Easing.OutCubic); this.FadeIn(enter_duration, Easing.OutQuint); this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutQuint); diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 07b5b53e0e..f92cfc2306 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -152,7 +152,6 @@ namespace osu.Game.Graphics.Containers protected override void PopOut() { - base.PopOut(); previewTrackManager.StopAnyPlaying(this); } diff --git a/osu.Game/Overlays/AccountCreationOverlay.cs b/osu.Game/Overlays/AccountCreationOverlay.cs index 6f79316670..ef2e055eae 100644 --- a/osu.Game/Overlays/AccountCreationOverlay.cs +++ b/osu.Game/Overlays/AccountCreationOverlay.cs @@ -90,7 +90,6 @@ namespace osu.Game.Overlays protected override void PopIn() { - base.PopIn(); this.FadeIn(transition_time, Easing.OutQuint); if (welcomeScreen.GetChildScreen() != null) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 96dbfe31f3..87df08ceec 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -276,8 +276,6 @@ namespace osu.Game.Overlays protected override void PopIn() { - base.PopIn(); - this.MoveToY(0, transition_length, Easing.OutQuint); this.FadeIn(transition_length, Easing.OutQuint); } diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 098a5d0a33..005162bbcc 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -99,7 +99,6 @@ namespace osu.Game.Overlays protected override void PopIn() { - base.PopIn(); lowPassFilter.CutoffTo(300, 100, Easing.OutCubic); } diff --git a/osu.Game/Overlays/LoginOverlay.cs b/osu.Game/Overlays/LoginOverlay.cs index 536811dfcf..8b60024682 100644 --- a/osu.Game/Overlays/LoginOverlay.cs +++ b/osu.Game/Overlays/LoginOverlay.cs @@ -75,8 +75,6 @@ namespace osu.Game.Overlays protected override void PopIn() { - base.PopIn(); - panel.Bounding = true; this.FadeIn(transition_time, Easing.OutQuint); diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index bd895fe6bf..eba35ec6f9 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -246,9 +246,13 @@ namespace osu.Game.Overlays } } + protected override void PopIn() + { + this.FadeIn(200); + } + protected override void PopOut() { - base.PopOut(); this.FadeOut(200); } diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index 7f7b09a62c..a372ec70db 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -130,7 +130,6 @@ namespace osu.Game.Overlays.Mods { const double fade_in_duration = 400; - base.PopIn(); this.FadeIn(fade_in_duration, Easing.OutQuint); Header.MoveToY(0, fade_in_duration, Easing.OutQuint); diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index f2eefb6e4b..15e6c94b34 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -206,8 +206,6 @@ namespace osu.Game.Overlays protected override void PopIn() { - base.PopIn(); - this.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint); mainContent.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint); mainContent.FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out); diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index e3e3b4bd80..15eefb2d9f 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -229,8 +229,6 @@ namespace osu.Game.Overlays protected override void PopIn() { - base.PopIn(); - this.FadeIn(transition_length, Easing.OutQuint); dragContainer.ScaleTo(1, transition_length, Easing.OutElastic); } diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 1681187f82..d7f39a9d8f 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -163,8 +163,6 @@ namespace osu.Game.Overlays protected override void PopIn() { - base.PopIn(); - ContentContainer.MoveToX(ExpandedPosition, TRANSITION_LENGTH, Easing.OutQuint); SectionsContainer.FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out); diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs index 00474cc0d8..34fbec93b7 100644 --- a/osu.Game/Overlays/WaveOverlayContainer.cs +++ b/osu.Game/Overlays/WaveOverlayContainer.cs @@ -34,8 +34,6 @@ namespace osu.Game.Overlays protected override void PopIn() { - base.PopIn(); - Waves.Show(); this.FadeIn(100, Easing.OutQuint); } diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs index 4d4fe4ea56..05232fe0e2 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs @@ -54,14 +54,12 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components protected override void PopIn() { - base.PopIn(); Settings.MoveToY(0, TRANSITION_DURATION, Easing.OutQuint); Settings.FadeIn(TRANSITION_DURATION / 2); } protected override void PopOut() { - base.PopOut(); Settings.MoveToY(-1, TRANSITION_DURATION, Easing.InSine); Settings.Delay(TRANSITION_DURATION / 2).FadeOut(TRANSITION_DURATION / 2); } diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs index c92dc2e343..5753c268d9 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs @@ -86,8 +86,6 @@ namespace osu.Game.Screens.Select.Options protected override void PopIn() { - base.PopIn(); - this.FadeIn(transition_duration, Easing.OutQuint); if (buttonsContainer.Position.X == 1 || Alpha == 0) From d9c00fc4be6aeb7fba570ccee75e39095d0363f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 18 Jun 2023 20:57:43 +0900 Subject: [PATCH 0236/2100] Update framework --- 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 f4d08e443c..522d28dca7 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -11,7 +11,7 @@ manifestmerger.jar - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index e08b09aef9..4b9f37270b 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 9aafec6c50..96396ca4ad 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -16,6 +16,6 @@ iossimulator-x64 - + From 62f01e4f4008421280925c7bd26608f084c10ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 13:49:29 +0200 Subject: [PATCH 0237/2100] Rename `ModState` members to better convey what's what --- osu.Game/Overlays/Mods/ModColumn.cs | 2 +- osu.Game/Overlays/Mods/ModPanel.cs | 8 ++++---- osu.Game/Overlays/Mods/ModState.cs | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 610fd4e935..0845edf7f8 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Mods foreach (var mod in availableMods) { mod.Active.BindValueChanged(_ => updateState()); - mod.MatchingFilter.BindValueChanged(_ => updateState()); + mod.MatchingTextFilter.BindValueChanged(_ => updateState()); mod.ValidForSelection.BindValueChanged(_ => updateState()); } diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index 86ecdfa31d..3f85e0b5eb 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -57,7 +57,7 @@ namespace osu.Game.Overlays.Mods base.LoadComplete(); modState.ValidForSelection.BindValueChanged(_ => updateFilterState()); - modState.MatchingFilter.BindValueChanged(_ => updateFilterState(), true); + modState.MatchingTextFilter.BindValueChanged(_ => updateFilterState(), true); } protected override void Select() @@ -100,13 +100,13 @@ namespace osu.Game.Overlays.Mods public override bool MatchingFilter { - get => modState.MatchingFilter.Value; + get => modState.MatchingTextFilter.Value; set { - if (modState.MatchingFilter.Value == value) + if (modState.MatchingTextFilter.Value == value) return; - modState.MatchingFilter.Value = value; + modState.MatchingTextFilter.Value = value; } } diff --git a/osu.Game/Overlays/Mods/ModState.cs b/osu.Game/Overlays/Mods/ModState.cs index 5e0d768021..1ec517ca11 100644 --- a/osu.Game/Overlays/Mods/ModState.cs +++ b/osu.Game/Overlays/Mods/ModState.cs @@ -39,14 +39,14 @@ namespace osu.Game.Overlays.Mods public BindableBool ValidForSelection { get; } = new BindableBool(true); /// - /// Whether the is passing all filters and visible for user + /// Whether the mod is matching the current textual filter. /// - public bool Visible => MatchingFilter.Value && ValidForSelection.Value; + public BindableBool MatchingTextFilter { get; } = new BindableBool(true); /// - /// Whether the mod is matching the current filter, i.e. it is available for user selection. + /// Whether the matches all applicable filters and visible for the user to select. /// - public BindableBool MatchingFilter { get; } = new BindableBool(true); + public bool Visible => MatchingTextFilter.Value && ValidForSelection.Value; public ModState(Mod mod) { From c7e89905767a6cffc6437eece94ddde56d8c2365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 13:51:03 +0200 Subject: [PATCH 0238/2100] Remove unused property --- osu.Game/Overlays/Mods/ModPanel.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index 3f85e0b5eb..829a0886c3 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -77,18 +77,6 @@ namespace osu.Game.Overlays.Mods /// public bool Visible => modState.Visible; - public bool ValidForSelection - { - get => modState.ValidForSelection.Value; - set - { - if (modState.ValidForSelection.Value == value) - return; - - modState.ValidForSelection.Value = value; - } - } - #region Filtering support public override IEnumerable FilterTerms => new[] From 1b4d7db1e6254d55683ae9bbb14a7313a82cb088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 13:51:13 +0200 Subject: [PATCH 0239/2100] Remove redundant guard `Bindable` has one of those already. --- osu.Game/Overlays/Mods/ModPanel.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index 829a0886c3..14e5040767 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -89,13 +89,7 @@ namespace osu.Game.Overlays.Mods public override bool MatchingFilter { get => modState.MatchingTextFilter.Value; - set - { - if (modState.MatchingTextFilter.Value == value) - return; - - modState.MatchingTextFilter.Value = value; - } + set => modState.MatchingTextFilter.Value = value; } private void updateFilterState() From a1015b4145502fc5951548f2bdcd08de857f13e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 13:59:02 +0200 Subject: [PATCH 0240/2100] Remove duplicated xmldoc and move into relevant region --- osu.Game/Overlays/Mods/ModPanel.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index 14e5040767..f294b1892d 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -72,13 +72,11 @@ namespace osu.Game.Overlays.Mods Active.Value = false; } - /// - /// Whether the is passing all filters and visible for user - /// - public bool Visible => modState.Visible; - #region Filtering support + /// + public bool Visible => modState.Visible; + public override IEnumerable FilterTerms => new[] { Mod.Name, From 64e96c6d82772e19a4a8cc8733ef8a560eeb5ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 14:03:26 +0200 Subject: [PATCH 0241/2100] Fix duplicate linq and reword comment --- osu.Game/Overlays/Mods/ModColumn.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 0845edf7f8..60c1282a65 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -150,10 +150,12 @@ namespace osu.Game.Overlays.Mods if (toggleAllCheckbox != null && !SelectionAnimationRunning) { - toggleAllCheckbox.Alpha = availableMods.Any(panel => panel.Visible) ? 1 : 0; + bool anyPanelsVisible = availableMods.Any(panel => panel.Visible); - //Prevent checkbox from checking when column have on valid panels - if (availableMods.Any(panel => panel.Visible)) + toggleAllCheckbox.Alpha = anyPanelsVisible ? 1 : 0; + + // checking `anyPanelsVisible` is important since `.All()` returns `true` for empty enumerables. + if (anyPanelsVisible) toggleAllCheckbox.Current.Value = availableMods.Where(panel => panel.Visible).All(panel => panel.Active.Value); } } From 4c78144d10093b4ee3dcd718d06e7ca8b4b66e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 14:04:00 +0200 Subject: [PATCH 0242/2100] Remove obvious comment --- osu.Game/Overlays/Mods/ModColumn.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 60c1282a65..d65c94d14d 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -215,7 +215,7 @@ namespace osu.Game.Overlays.Mods foreach (var button in availableMods.Where(b => b.Active.Value)) { if (!button.Visible) - button.Active.Value = false; //If mod panel is hidden change state manually without any animation + button.Active.Value = false; else pendingSelectionOperations.Enqueue(() => button.Active.Value = false); } From 9758e5f840c88b3dc39745a4bd8f7841ef9fcc44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 14:16:31 +0200 Subject: [PATCH 0243/2100] Fix utterly broken test - Was on wrong ruleset, so the mod/free mod sets did literally nothing - `assertHasFreeModButton` had a param that did nothing - Was checking `MatchingFilter` rather than `Visible` --- .../TestSceneMultiplayerMatchSongSelect.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 23090e9da4..947b7e5be6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -94,6 +94,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [TestCase(typeof(OsuModHidden), typeof(OsuModTraceable))] // Incompatible. public void TestAllowedModDeselectedWhenRequired(Type allowedMod, Type requiredMod) { + AddStep("change ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo); AddStep($"select {allowedMod.ReadableName()} as allowed", () => songSelect.FreeMods.Value = new[] { (Mod)Activator.CreateInstance(allowedMod) }); AddStep($"select {requiredMod.ReadableName()} as required", () => songSelect.Mods.Value = new[] { (Mod)Activator.CreateInstance(requiredMod) }); @@ -102,17 +103,17 @@ namespace osu.Game.Tests.Visual.Multiplayer // A previous test's mod overlay could still be fading out. AddUntilStep("wait for only one freemod overlay", () => this.ChildrenOfType().Count() == 1); - assertHasFreeModButton(allowedMod, false); - assertHasFreeModButton(requiredMod, false); + assertFreeModNotShown(allowedMod); + assertFreeModNotShown(requiredMod); } - private void assertHasFreeModButton(Type type, bool hasButton = true) + private void assertFreeModNotShown(Type type) { - AddAssert($"{type.ReadableName()} {(hasButton ? "displayed" : "not displayed")} in freemod overlay", + AddAssert($"{type.ReadableName()} not displayed in freemod overlay", () => this.ChildrenOfType() .Single() .ChildrenOfType() - .Where(panel => panel.MatchingFilter) + .Where(panel => panel.Visible) .All(b => b.Mod.GetType() != type)); } From 76f509a1db5d57601595b3c050d3ba9458e3383e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 14:19:38 +0200 Subject: [PATCH 0244/2100] Do not use `?? true` pattern Universally disliked. `!= false` is preferred. --- osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index 255dbfcdd3..18739c0275 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -334,7 +334,7 @@ namespace osu.Game.Tests.Visual.UserInterface private void setFilter(Func? filter) { foreach (var modState in this.ChildrenOfType().Single().AvailableMods) - modState.ValidForSelection.Value = filter?.Invoke(modState.Mod) ?? true; + modState.ValidForSelection.Value = filter?.Invoke(modState.Mod) != false; } private partial class TestModColumn : ModColumn From b9156b1df312789c17b8815120e84b19f096784b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 14:24:35 +0200 Subject: [PATCH 0245/2100] Reword/rename some stuff in test --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index ffc0a0a0ad..868ee2c73c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -678,10 +678,10 @@ namespace osu.Game.Tests.Visual.UserInterface } /// - /// Internal search applies from code by setting + /// Covers columns hiding/unhiding on changes of . /// [Test] - public void TestColumnHidingOnInternalSearch() + public void TestColumnHidingOnIsValidChange() { AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay { @@ -711,10 +711,10 @@ namespace osu.Game.Tests.Visual.UserInterface } /// - /// External search applies by user by entering search term into search bar + /// Covers columns hiding/unhiding on changes of . /// [Test] - public void TestColumnHidingOnExternalSearch() + public void TestColumnHidingOnTextFilterChange() { AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay { @@ -738,7 +738,7 @@ namespace osu.Game.Tests.Visual.UserInterface } [Test] - public void TestHidingOverlayClearsSearch() + public void TestHidingOverlayClearsTextSearch() { AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay { From 28f929dc4db740c5037c99d77f36791927a36f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 14:28:26 +0200 Subject: [PATCH 0246/2100] Remove yet another redundant guard --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 574d18de0e..a741a7a005 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -70,13 +70,7 @@ namespace osu.Game.Overlays.Mods public string SearchTerm { get => SearchTextBox.Current.Value; - set - { - if (SearchTextBox.Current.Value == value) - return; - - SearchTextBox.Current.Value = value; - } + set => SearchTextBox.Current.Value = value; } public ShearedSearchTextBox SearchTextBox { get; private set; } = null!; From a49af06e883d4270cca6061c8c8d8864be6283af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 14:34:33 +0200 Subject: [PATCH 0247/2100] Reword comments in `ModSelectOverlay` --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index a741a7a005..b12b1a7df4 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -247,7 +247,7 @@ namespace osu.Game.Overlays.Mods { base.Hide(); - //We want to clear search for next user interaction with mod overlay + // clear search for next user interaction with mod overlay SearchTextBox.Current.Value = string.Empty; } @@ -615,7 +615,9 @@ namespace osu.Game.Overlays.Mods hideOverlay(true); return true; - //This is handled locally here to prevent search box from coupling in DeselectAllModsButton + // This is handled locally here due to conflicts in input handling between the search text box and the deselect all mods button. + // Attempting to handle this action locally in both places leads to a possible scenario + // wherein activating the binding will both change the contents of the search text box and deselect all mods. case GlobalAction.DeselectAllMods: { if (!SearchTextBox.HasFocus) @@ -664,7 +666,9 @@ namespace osu.Game.Overlays.Mods /// /// - /// This is handled locally here to allow handle first + /// This is handled locally here due to conflicts in input handling between the search text box and the select all mods button. + /// Attempting to handle this action locally in both places leads to a possible scenario + /// wherein activating the "select all" platform binding will both select all text in the search box and select all mods. /// > public bool OnPressed(KeyBindingPressEvent e) { @@ -832,8 +836,7 @@ namespace osu.Game.Overlays.Mods if (!Active.Value) RequestScroll?.Invoke(this); - //By doing this we kill the focus on SearchTextBox. - //Killing focus is done here because it's the only feasible place on ModSelectOverlay you can click on without triggering any action. + // Killing focus is done here because it's the only feasible place on ModSelectOverlay you can click on without triggering any action. Scheduler.Add(() => GetContainingInputManager().ChangeFocus(null)); return true; From 0b6c0592e4ee8ab28babdcc3384135e86b3d2145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 14:45:43 +0200 Subject: [PATCH 0248/2100] Add failing test case for mod preset filtering not working after ruleset change --- .../UserInterface/TestSceneModPresetColumn.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index 3efdba8754..2d54a4e566 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -392,6 +392,28 @@ namespace osu.Game.Tests.Visual.UserInterface new HashSet(this.ChildrenOfType().First().Preset.Value.Mods).SetEquals(mods)); } + [Test] + public void TestTextFiltering() + { + ModPresetColumn modPresetColumn = null!; + + AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); + AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded); + + AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0)); + AddStep("set text filter", () => modPresetColumn.SearchTerm = "First"); + AddUntilStep("one panel visible", () => modPresetColumn.ChildrenOfType().Count(panel => panel.IsPresent), () => Is.EqualTo(1)); + + AddStep("set mania ruleset", () => Ruleset.Value = rulesets.GetRuleset(3)); + AddUntilStep("no panels visible", () => modPresetColumn.ChildrenOfType().Count(panel => panel.IsPresent), () => Is.EqualTo(0)); + } + private ICollection createTestPresets() => new[] { new ModPreset From d4c9eb013e22290fe0b2998abdbf69073aee281e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 14:51:50 +0200 Subject: [PATCH 0249/2100] Fix bugged initial state of matching filter flag Was preventing mod preset panels from refiltering correctly on ruleset change due to the `matchingFilter == value` guard. --- osu.Game/Overlays/Mods/ModSelectPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index cc13657c04..a69fb19c4c 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -286,7 +286,7 @@ namespace osu.Game.Overlays.Mods public abstract IEnumerable FilterTerms { get; } - private bool matchingFilter; + private bool matchingFilter = true; public virtual bool MatchingFilter { From 75300ca2295bce92b70e23450a1b1ffe61efa221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 15:50:25 +0200 Subject: [PATCH 0250/2100] Switch search box to initially unfocused Done primarily to keep mod hotkeys working without any behavioural changes when mod select is opened. --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 10 +++++----- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 -- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 868ee2c73c..d566a04261 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -550,12 +550,12 @@ namespace osu.Game.Tests.Visual.UserInterface { createScreen(); - AddStep("click on mod column", navigateAndClick); - AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus); - AddStep("click on search", navigateAndClick); AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus); + AddStep("click on mod column", navigateAndClick); + AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus); + void navigateAndClick() where T : Drawable { InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().FirstOrDefault()); @@ -571,10 +571,10 @@ namespace osu.Game.Tests.Visual.UserInterface const Key focus_switch_key = Key.Tab; AddStep("press tab", () => InputManager.Key(focus_switch_key)); - AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus); + AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus); AddStep("press tab", () => InputManager.Key(focus_switch_key)); - AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus); + AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus); } [Test] diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index b12b1a7df4..23e278e378 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -500,8 +500,6 @@ namespace osu.Game.Overlays.Mods base.PopIn(); - SearchTextBox.TakeFocus(); - aboveColumnsContent .FadeIn(fade_in_duration, Easing.OutQuint) .MoveToY(0, fade_in_duration, Easing.OutQuint); From b87acfa66feb51787b9a46a207d84af762f82865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 15:53:49 +0200 Subject: [PATCH 0251/2100] Dynamically change placeholder to convey how to activate search --- osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs | 7 +++++++ osu.Game/Localisation/ModSelectOverlayStrings.cs | 9 +++++++-- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 7 +++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs index a6954fafb1..fb0a66cb8d 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -37,6 +38,12 @@ namespace osu.Game.Graphics.UserInterface set => textBox.HoldFocus = value; } + public LocalisableString PlaceholderText + { + get => textBox.PlaceholderText; + set => textBox.PlaceholderText = value; + } + public new bool HasFocus => textBox.HasFocus; public void TakeFocus() => textBox.TakeFocus(); diff --git a/osu.Game/Localisation/ModSelectOverlayStrings.cs b/osu.Game/Localisation/ModSelectOverlayStrings.cs index f11c52ee20..05dcf138d7 100644 --- a/osu.Game/Localisation/ModSelectOverlayStrings.cs +++ b/osu.Game/Localisation/ModSelectOverlayStrings.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Localisation; @@ -39,6 +39,11 @@ namespace osu.Game.Localisation /// public static LocalisableString UseCurrentMods => new TranslatableString(getKey(@"use_current_mods"), @"Use current mods"); + /// + /// "tab to search..." + /// + public static LocalisableString TabToSearch => new TranslatableString(getKey(@"tab_to_search"), @"tab to search..."); + private static string getKey(string key) => $@"{prefix}:{key}"; } -} +} \ No newline at end of file diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 23e278e378..2f39758982 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -303,6 +303,13 @@ namespace osu.Game.Overlays.Mods }); } + protected override void Update() + { + base.Update(); + + SearchTextBox.PlaceholderText = SearchTextBox.HasFocus ? Resources.Localisation.Web.CommonStrings.InputSearch : ModSelectOverlayStrings.TabToSearch; + } + /// /// Select all visible mods in all columns. /// From b4c1266fc5ca746cb0ea0c50b0093bc74796038b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 15:56:15 +0200 Subject: [PATCH 0252/2100] Add TODO for future support of typical search shortcuts --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 2f39758982..9035503723 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -693,6 +693,7 @@ namespace osu.Game.Overlays.Mods if (e.Repeat || e.Key != Key.Tab) return false; + // TODO: should probably eventually support typical platform search shortcuts (`Ctrl-F`, `/`) if (SearchTextBox.HasFocus) SearchTextBox.KillFocus(); else From 886a1e98da66b381244c114d271e8951456b2a9f Mon Sep 17 00:00:00 2001 From: tsrk Date: Sun, 18 Jun 2023 17:32:49 +0200 Subject: [PATCH 0253/2100] test(SkinDeserialisationTest): use more complete skin --- .../Archives/modified-argon-pro-20230616.osk | Bin 1234 -> 0 bytes .../Archives/modified-argon-pro-20230618.osk | Bin 0 -> 1628 bytes osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 osu.Game.Tests/Resources/Archives/modified-argon-pro-20230616.osk create mode 100644 osu.Game.Tests/Resources/Archives/modified-argon-pro-20230618.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-pro-20230616.osk b/osu.Game.Tests/Resources/Archives/modified-argon-pro-20230616.osk deleted file mode 100644 index 5e5d7960dd585f9ed89c16e4b961e12fe968ace3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1234 zcmWIWW@Zs#U|`^2V3>R%?D;~^O|yZ#?GO=$;_S>kz0AB!)iZ{CO%4KX59hU5{|RzT zIudXx@@8aast#LOL+@_mlt?Fw*{$xMxRTb~pZ>ja?`fgyjTcuI?}~VNah1!oj#jk` zGFOAQ=vr=X>Pc))YxVogm&RwG_jiTjyNegLxQdos+c+V8%KnuP8O~4SakW}<^6!~L zjdzrN^=7a9zq-e1{iH;mu02*^p^bM|Z=9PNyyRqNUvrz3d1aU0B(?Ye6b~r1KeVm_ zI({+`^8zu*1DSbg`FdH!`FWzmv5J$tr%PSS)F z!{~CK4eDkWl6;$%OuBL*X~WwabN36D&lU;jc{fA)dPZf&i}O2m?#ZJ%(z=XQEtiRb z!4(+rVn9dwCT8Y>|EA=N9DWrRJ3sgI)VJJU0K9fk^HAE&l~R@`U;P2vGZYAVK=F z3GdW0{ZLWMhe9?zz3XIet$0;GpLyW~-P(n^OKv@2om>22?~flbkA8oDFSFdtrR=m5 zWBgN}t9BOOB{LpIteC*b9h1+Pemv*K=@Qmyt2O6N(9hva5mIcplA;sguPI%mPj+P}}SYNeP$wUOYW zF#C+eH74d|owesJ3)2N}En4z&SKjKl1wr1GxsTo*Y(K23KPB| zDtUjUbnfeU#%1jJUEp7%&9lhZ{{nk2EwKM=J;Sap`&dhxQ*r{&^o{4Lv_+)O?3|&L zk{x&d&4;~@G>p|JI$7+UnRipHI&W&O&Y{rH8|7U&H)+`Jo!Vqz#J=AL zr@Wq8s`fk3-{h;*v;GE7WBI)7`PN6Ta%_x$RblF0D4(pV?sY6V#IWhw^KBa(Wx&-KJdY)tX=bly$y-%lGl^ z>d?j?m(qUL7i^xyC|CHIGw@;ghIv_^CMGgkmd{yjI5D$w#>?*3pW#NmiJX^Mn!GvA z%yH+=T)t9t+rKxKK~Aa?Ej-Pp*KM9O%{H0O==wCxMd#|RX*@TiCL9e|)t!^rl5nzs zQ6gh<-nM=**QQp5Z%;Jk1plqveAD7vLDky-?Vowti+L=9ccd6KZG3d;Mb{k{YuQI> z=Ck}npFNA-(>L$4;BrUX-xDss3;OlF&OBqYr_HPK)eqKOx9r}0o8NrrhP2DaZ!SKx ziGM$1fHxzP2m|hd0GLW4pbE-AXFDTFQRKj&l3o(X^gmXNPssh Q8%P~95N-w1>?|N200~+DYybcN diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-pro-20230618.osk b/osu.Game.Tests/Resources/Archives/modified-argon-pro-20230618.osk new file mode 100644 index 0000000000000000000000000000000000000000..dd25e06c06896c54d4ccbff5448d54d8252d9bb7 GIT binary patch literal 1628 zcmWIWW@Zs#U|`^2INE(F?D;~^O|yZ#?O+iGhT`nZJiW}kOw}`nd`%7lZV%_RSpNxf zOga*9De`7yW~vTbSwrt`x~yz7VnC9d2yA?w2oG_ z3o=)Ox9D1KZ|X^GPHXl1%$LSzpZ9l#;=79%wz!IxT-!Jyeailo4;ju+wLi43 z0y=&&5c2{t$OD;qY596t#rb)ry&Jg>IS8=+cKDZgSF5q$#MH7R#Y`80-6q^STU4Lq z?wAwN@qeBA#@fSsDyOmv#;5SrC~W(>adDq&Ut3>hr`y!~)$zfm(d(Pr_C0&Hd`{AY z6vOCppAG6}7m|FNmQ1>GA!)5JL>6@6D=Mn1SoS$2epO>0fQVe$O+wj2rTLuDk^SAsL_{bCHq#!%FB4I;s z=Bzfi-P2cf%_&fk>+3z|cYDRF`t{5UCur`uz_ilrQ`5fRkF5XPEx*D4{(fXwa?6)K zHko~$J9BEb$Q4~UAUK`zDcg=X=4)!3y5iEj4K}ugX#VsPR=6y{*rFutXKb`jYNFto z%u`De3cpH?CdA_iz%pgiuqbksRFUO_BTO#4qseP zRW9Bce8@#>*;%36v$_xLb)3`Zd}~{7`K+Y@X-;|5Z$y76EI%0^bUEqdo@H}Sm@JW5 zWjwXRl`lQNtC1tQ)ADJO_`gn_q{@5u^s1IR)Fys6sIJTQ+u0P9xWH6+;;C$HRjFe= z=LJ5QuFp67F|BaIjLQK^bK-ngOHer)(v{I<q^tSA@t&ex?&1-!X z+yCXv@?hrQR&QVZe^92&Ym@fNc+nC01e4Gw!bgRQ@~p2XD_LJnD~mgOrv6eS3n$0h zVzmR;4Y|b+`2;(^`4d~LvD4=wzs9faJ{ubfmu*gBn=*5?;KUQrlP@jb-eA(?6_+8T z{Dbo->w@(q+iuw{_jzQ(u#b1%qmB>$U(;S+cvfQlfA(jdvzCl=ynR-1Br46kuu9E5 z@Vs(a-Qth049xF8D*ZCYeNNbR-I&weW9UEC(6)O0PYbKi_@nycP0hjHpRC}j!S{?GV5 zrFWCxqlEJC2&+rpeGg6@ylueq?$B@M_kCaY-8eP(Ud651sX`Cm%sRQ#lrJvgqf6w` zUHO$3$L2>(?ehDuo$>RkX%pTb=jvp0J9i+eJij>U+^*H%<1a5bvmoh=Blqb=juY5g zSd)9r%i;{a3Qy*~_Sh?N-NJIq|FRX^dChiq@2(HVg++=vvVW7=+d|V3cAnZqUs@PXGurxL7cfLx49c8%O~Q5PkyEA*>)C E0DO_V8vp From c8afd057bd2e8781748d9c0a3777773a9647b967 Mon Sep 17 00:00:00 2001 From: tsrk Date: Sun, 18 Jun 2023 17:51:17 +0200 Subject: [PATCH 0254/2100] test(TestSceneOsuTouchInput): simplify draw hierarchy An InputTrigger is considered active as long as --- .../TestSceneOsuTouchInput.cs | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs index 4572970011..9510935a31 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs @@ -1,7 +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.Diagnostics; using System.Linq; using NUnit.Framework; @@ -39,8 +38,6 @@ namespace osu.Game.Rulesets.Osu.Tests private DefaultKeyCounter rightKeyCounter = null!; - private KeyCounterController controller = null!; - private OsuInputManager osuInputManager = null!; private Container mainContent = null!; @@ -52,37 +49,31 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Create tests", () => { + InputTrigger triggerLeft; + InputTrigger triggerRight; + Children = new Drawable[] { osuInputManager = new OsuInputManager(new OsuRuleset().RulesetInfo) { - Children = new Drawable[] + Child = mainContent = new Container { - controller = new KeyCounterController(), - mainContent = new DependencyProvidingContainer + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - CachedDependencies = new (Type, object)[] { (typeof(KeyCounterController), controller) }, - Children = new Drawable[] + new OsuCursorContainer { - new OsuCursorContainer - { - Depth = float.MinValue, - } + Depth = float.MinValue, }, + triggerLeft = new TestActionKeyCounterTrigger(OsuAction.LeftButton), + triggerRight = new TestActionKeyCounterTrigger(OsuAction.RightButton) }, - } + }, }, new TouchVisualiser(), }; - InputTrigger triggerLeft; - InputTrigger triggerRight; - - controller.Add(triggerLeft = new TestActionKeyCounterTrigger(OsuAction.LeftButton)); - controller.Add(triggerRight = new TestActionKeyCounterTrigger(OsuAction.RightButton)); - mainContent.AddRange(new[] { leftKeyCounter = new DefaultKeyCounter(triggerLeft) From 425d3c23f5265b136c9a385304897db208b9b6b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Jun 2023 01:02:16 +0900 Subject: [PATCH 0255/2100] Fix some code layout and NRT some classes --- osu.Game/Overlays/Mods/ModState.cs | 2 -- osu.Game/Overlays/Mods/SelectAllModsButton.cs | 2 -- osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs | 13 +++++-------- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModState.cs b/osu.Game/Overlays/Mods/ModState.cs index 1ec517ca11..7a5bc0f3ae 100644 --- a/osu.Game/Overlays/Mods/ModState.cs +++ b/osu.Game/Overlays/Mods/ModState.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Game.Rulesets.Mods; diff --git a/osu.Game/Overlays/Mods/SelectAllModsButton.cs b/osu.Game/Overlays/Mods/SelectAllModsButton.cs index dd14514a3b..bb61cdc35d 100644 --- a/osu.Game/Overlays/Mods/SelectAllModsButton.cs +++ b/osu.Game/Overlays/Mods/SelectAllModsButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index d5e57b9ec9..4d5d724089 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Game.Overlays; using System.Collections.Generic; @@ -36,11 +34,10 @@ namespace osu.Game.Screens.OnlinePlay protected override IEnumerable CreateFooterButtons() => base.CreateFooterButtons() - .Prepend( - SelectAllModsButton = new SelectAllModsButton(this) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - }); + .Prepend(SelectAllModsButton = new SelectAllModsButton(this) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }); } } From db445660e763b510ce4a7214e9f8614045a87546 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Jun 2023 01:06:45 +0900 Subject: [PATCH 0256/2100] Avoid resolving realm `Live` more than once --- osu.Game/Overlays/Mods/ModPresetPanel.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index 607d236781..00f6e36972 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -88,10 +88,12 @@ namespace osu.Game.Overlays.Mods private IEnumerable getFilterTerms() { - yield return Preset.Value.Name; - yield return Preset.Value.Description; + var preset = Preset.Value; - foreach (Mod mod in Preset.Value.Mods) + yield return preset.Name; + yield return preset.Description; + + foreach (Mod mod in preset.Mods) { yield return mod.Name; yield return mod.Acronym; From cf1ee2ba35c69444b99ddbdd103a97154874c29f Mon Sep 17 00:00:00 2001 From: tsrk Date: Sun, 18 Jun 2023 18:26:08 +0200 Subject: [PATCH 0257/2100] test(TestSceneOsuTouchInput): fix `InputTrigger` depth --- osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs index 9510935a31..2e62689e2c 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs @@ -66,8 +66,14 @@ namespace osu.Game.Rulesets.Osu.Tests { Depth = float.MinValue, }, - triggerLeft = new TestActionKeyCounterTrigger(OsuAction.LeftButton), + triggerLeft = new TestActionKeyCounterTrigger(OsuAction.LeftButton) + { + Depth = float.MinValue + }, triggerRight = new TestActionKeyCounterTrigger(OsuAction.RightButton) + { + Depth = float.MinValue + } }, }, }, @@ -80,14 +86,12 @@ namespace osu.Game.Rulesets.Osu.Tests { Anchor = Anchor.Centre, Origin = Anchor.CentreRight, - Depth = float.MinValue, X = -100, }, rightKeyCounter = new DefaultKeyCounter(triggerRight) { Anchor = Anchor.Centre, Origin = Anchor.CentreLeft, - Depth = float.MinValue, X = 100, }, }); From bd174b5193f06f81dea4da34fb3e3d596fe39d1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 18:37:04 +0200 Subject: [PATCH 0258/2100] Revert to non-bindable `AccentColour` Not necessary for now, so let's not incur unnecessary overheads. --- .../TestSceneTournamentMatchChatDisplay.cs | 4 +-- osu.Game/Overlays/Chat/ChatLine.cs | 27 +++++++++---------- .../Overlays/Chat/DrawableChatUsername.cs | 12 +++------ 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs index 71d8cbeea7..fada340cf7 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs @@ -90,7 +90,7 @@ namespace osu.Game.Tournament.Tests.Components })); AddUntilStep("message from team red is red color", () => - this.ChildrenOfType().Any(s => s.AccentColour.Value == TournamentGame.COLOUR_RED)); + this.ChildrenOfType().Any(s => s.AccentColour == TournamentGame.COLOUR_RED)); AddStep("message from team red", () => testChannel.AddNewMessages(new Message(nextMessageId()) { @@ -105,7 +105,7 @@ namespace osu.Game.Tournament.Tests.Components })); AddUntilStep("message from team blue is blue color", () => - this.ChildrenOfType().Any(s => s.AccentColour.Value == TournamentGame.COLOUR_BLUE)); + this.ChildrenOfType().Any(s => s.AccentColour == TournamentGame.COLOUR_BLUE)); AddStep("message from admin", () => testChannel.AddNewMessages(new Message(nextMessageId()) { diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 2931685239..13f26e6fcc 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -93,19 +93,6 @@ namespace osu.Game.Overlays.Chat configManager.BindWith(OsuSetting.Prefer24HourTime, prefer24HourTime); prefer24HourTime.BindValueChanged(_ => updateTimestamp()); - drawableUsername = new DrawableChatUsername(message.Sender) - { - Width = UsernameWidth, - FontSize = FontSize, - AutoSizeAxes = Axes.Y, - Origin = Anchor.TopRight, - Anchor = Anchor.TopRight, - Margin = new MarginPadding { Horizontal = Spacing }, - Inverted = !string.IsNullOrEmpty(message.Sender.Colour), - }; - - drawableUsername.AccentColour.Value = UsernameColour; - InternalChild = new GridContainer { RelativeSizeAxes = Axes.X, @@ -129,7 +116,17 @@ namespace osu.Game.Overlays.Chat Font = OsuFont.GetFont(size: FontSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true), AlwaysPresent = true, }, - drawableUsername, + drawableUsername = new DrawableChatUsername(message.Sender) + { + Width = UsernameWidth, + FontSize = FontSize, + AutoSizeAxes = Axes.Y, + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + Margin = new MarginPadding { Horizontal = Spacing }, + AccentColour = UsernameColour, + Inverted = !string.IsNullOrEmpty(message.Sender.Colour), + }, drawableContentFlow = new LinkFlowContainer(styleMessageContent) { AutoSizeAxes = Axes.Y, @@ -175,7 +172,7 @@ namespace osu.Game.Overlays.Chat CornerRadius = 2f, Masking = true, RelativeSizeAxes = Axes.Both, - Colour = drawableUsername.AccentColour.Value.Darken(1f), + Colour = drawableUsername.AccentColour.Darken(1f), Depth = float.MaxValue, Child = new Box { RelativeSizeAxes = Axes.Both } }); diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index 05772051da..cb5df93e00 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Chat { public Action? ReportRequested; - public Bindable AccentColour { get; } = new Bindable(); + public Color4 AccentColour { get; init; } public bool Inverted { get; init; } @@ -146,11 +146,7 @@ namespace osu.Game.Overlays.Chat { base.LoadComplete(); drawableText.Colour = colours.ChatBlue; - - AccentColour.BindValueChanged(c => - { - colouredDrawable.Colour = c.NewValue; - }, true); + colouredDrawable.Colour = AccentColour; } public MenuItem[] ContextMenuItems @@ -196,7 +192,7 @@ namespace osu.Game.Overlays.Chat protected override bool OnHover(HoverEvent e) { - colouredDrawable.FadeColour(AccentColour.Value.Lighten(0.6f), 30, Easing.OutQuint); + colouredDrawable.FadeColour(AccentColour.Lighten(0.6f), 30, Easing.OutQuint); return base.OnHover(e); } @@ -205,7 +201,7 @@ namespace osu.Game.Overlays.Chat { base.OnHoverLost(e); - colouredDrawable.FadeColour(AccentColour.Value, 800, Easing.OutQuint); + colouredDrawable.FadeColour(AccentColour, 800, Easing.OutQuint); } } } From dad32817ee49bece00f2cf42e9648e4cbe9e293c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 18:37:28 +0200 Subject: [PATCH 0259/2100] Improve `UsernameColour` documentation --- osu.Game/Overlays/Chat/ChatLine.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 13f26e6fcc..b2024e15c7 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -70,8 +70,16 @@ namespace osu.Game.Overlays.Chat private Container? highlight; /// - /// if set, it will override or . + /// The colour to use to paint the chat mesasge author's username. /// + /// + /// The colour can be set explicitly by consumers via the property initialiser. + /// If unspecified, the colour is by default initialised to: + /// + /// message.Sender.Colour, if non-empty, + /// a random colour from if the above is empty. + /// + /// public Color4 UsernameColour { get; init; } public ChatLine(Message message) @@ -81,7 +89,8 @@ namespace osu.Game.Overlays.Chat RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - // If we have custom value, this value will be override. + // initialise using sane defaults. + // consumers can use the initialiser of `UsernameColour` to override this if they wish to. UsernameColour = !string.IsNullOrEmpty(message.Sender.Colour) ? Color4Extensions.FromHex(message.Sender.Colour) : default_colours[message.SenderId % default_colours.Length]; From a2a9823d8400513ed20cdf895931a15c78f8dcc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 18:43:55 +0200 Subject: [PATCH 0260/2100] Rename constant --- osu.Game/Overlays/Chat/ChatLine.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index b2024e15c7..fdf91dce23 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Chat /// If unspecified, the colour is by default initialised to: /// /// message.Sender.Colour, if non-empty, - /// a random colour from if the above is empty. + /// a random colour from if the above is empty. /// /// public Color4 UsernameColour { get; init; } @@ -93,7 +93,7 @@ namespace osu.Game.Overlays.Chat // consumers can use the initialiser of `UsernameColour` to override this if they wish to. UsernameColour = !string.IsNullOrEmpty(message.Sender.Colour) ? Color4Extensions.FromHex(message.Sender.Colour) - : default_colours[message.SenderId % default_colours.Length]; + : default_username_colours[message.SenderId % default_username_colours.Length]; } [BackgroundDependencyLoader] @@ -220,7 +220,7 @@ namespace osu.Game.Overlays.Chat drawableTimestamp.Text = message.Timestamp.LocalDateTime.ToLocalisableString(prefer24HourTime.Value ? @"HH:mm:ss" : @"hh:mm:ss tt"); } - private static readonly Color4[] default_colours = + private static readonly Color4[] default_username_colours = { Color4Extensions.FromHex("588c7e"), Color4Extensions.FromHex("b2a367"), From ee08ed414c1c4fa60458fd07fd96150c8d61b10b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 18:45:53 +0200 Subject: [PATCH 0261/2100] Document `DrawableChatUsername` members --- osu.Game/Overlays/Chat/DrawableChatUsername.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index cb5df93e00..21c3bd4b40 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -33,8 +33,15 @@ namespace osu.Game.Overlays.Chat { public Action? ReportRequested; + /// + /// The primary colour to use for the username. + /// public Color4 AccentColour { get; init; } + /// + /// If set to , the username will be drawn as plain text in . + /// If set to , the username will be drawn as black text inside a rounded rectangle in . + /// public bool Inverted { get; init; } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => From 1a6a66e953b1325561f7a8f0d289cfe8bcc6bdc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 18 Jun 2023 18:48:54 +0200 Subject: [PATCH 0262/2100] Rewrite assertions to be better --- .../Components/TestSceneTournamentMatchChatDisplay.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs index fada340cf7..b552d49d1d 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs @@ -4,6 +4,7 @@ #nullable disable using System.Linq; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -90,7 +91,7 @@ namespace osu.Game.Tournament.Tests.Components })); AddUntilStep("message from team red is red color", () => - this.ChildrenOfType().Any(s => s.AccentColour == TournamentGame.COLOUR_RED)); + this.ChildrenOfType().Last().AccentColour, () => Is.EqualTo(TournamentGame.COLOUR_RED)); AddStep("message from team red", () => testChannel.AddNewMessages(new Message(nextMessageId()) { @@ -105,7 +106,7 @@ namespace osu.Game.Tournament.Tests.Components })); AddUntilStep("message from team blue is blue color", () => - this.ChildrenOfType().Any(s => s.AccentColour == TournamentGame.COLOUR_BLUE)); + this.ChildrenOfType().Last().AccentColour, () => Is.EqualTo(TournamentGame.COLOUR_BLUE)); AddStep("message from admin", () => testChannel.AddNewMessages(new Message(nextMessageId()) { From 1a8219adf6e53f91e2b39487d4854825ff3853bb Mon Sep 17 00:00:00 2001 From: tsrk Date: Sun, 18 Jun 2023 20:20:56 +0200 Subject: [PATCH 0263/2100] style: guard event handler unsubscriptions --- osu.Game/Screens/Play/HUD/KeyCounter.cs | 8 ++++++-- osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/KeyCounter.cs b/osu.Game/Screens/Play/HUD/KeyCounter.cs index f12d2166fc..08d7e79e7c 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounter.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounter.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; namespace osu.Game.Screens.Play.HUD @@ -48,8 +49,11 @@ namespace osu.Game.Screens.Play.HUD { base.Dispose(isDisposing); - Trigger.OnActivate -= Activate; - Trigger.OnDeactivate -= Deactivate; + if (Trigger.IsNotNull()) + { + Trigger.OnActivate -= Activate; + Trigger.OnDeactivate -= Deactivate; + } } } } diff --git a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs index 8b92a5e3b8..efe51d75b0 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Rulesets.UI; @@ -77,7 +78,9 @@ namespace osu.Game.Screens.Play.HUD protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - controller.OnNewTrigger -= Add; + + if (controller.IsNotNull()) + controller.OnNewTrigger -= Add; } public bool UsesFixedAnchor { get; set; } From 141f9efad5cdf663c0d6fe1708dc8144eda272cc Mon Sep 17 00:00:00 2001 From: tsrk Date: Sun, 18 Jun 2023 21:26:16 +0200 Subject: [PATCH 0264/2100] style(KeyCounterController): remove reliance on `Receptor` --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 23 +------- .../Play/HUD/KeyCounterActionTrigger.cs | 16 ++--- .../Screens/Play/HUD/KeyCounterController.cs | 59 +------------------ osu.Game/Screens/Play/HUDOverlay.cs | 2 +- 4 files changed, 13 insertions(+), 87 deletions(-) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 889890e711..d3bc381f72 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -180,11 +180,8 @@ namespace osu.Game.Rulesets.UI private void attachKeyCounter(KeyCounterController keyCounter) { - var receptor = new ActionReceptor(keyCounter); + KeyBindingContainer.Add(keyCounter); - KeyBindingContainer.Add(receptor); - - keyCounter.SetReceptor(receptor); keyCounter.AddRange(KeyBindingContainer.DefaultKeyBindings .Select(b => b.GetAction()) .Distinct() @@ -192,24 +189,6 @@ namespace osu.Game.Rulesets.UI .Select(action => new KeyCounterActionTrigger(action))); } - private partial class ActionReceptor : KeyCounterController.Receptor, IKeyBindingHandler - { - public ActionReceptor(KeyCounterController target) - : base(target) - { - } - - public bool OnPressed(KeyBindingPressEvent e) => Target.Triggers - .OfType>() - .Any(c => c.OnPressed(e.Action, Clock.Rate >= 0)); - - public void OnReleased(KeyBindingReleaseEvent e) - { - foreach (var c in Target.Triggers.OfType>()) - c.OnReleased(e.Action, Clock.Rate >= 0); - } - } - #endregion #region Keys per second Counter Attachment diff --git a/osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs b/osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs index e5951a8bf4..f2c4487854 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs @@ -2,10 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; namespace osu.Game.Screens.Play.HUD { - public partial class KeyCounterActionTrigger : InputTrigger + public partial class KeyCounterActionTrigger : InputTrigger, IKeyBindingHandler where T : struct { public T Action { get; } @@ -16,21 +18,21 @@ namespace osu.Game.Screens.Play.HUD Action = action; } - public bool OnPressed(T action, bool forwards) + public bool OnPressed(KeyBindingPressEvent e) { - if (!EqualityComparer.Default.Equals(action, Action)) + if (!EqualityComparer.Default.Equals(e.Action, Action)) return false; - Activate(forwards); + Activate(Clock.Rate >= 0); return false; } - public void OnReleased(T action, bool forwards) + public void OnReleased(KeyBindingReleaseEvent e) { - if (!EqualityComparer.Default.Equals(action, Action)) + if (!EqualityComparer.Default.Equals(e.Action, Action)) return; - Deactivate(forwards); + Deactivate(Clock.Rate >= 0); } } } diff --git a/osu.Game/Screens/Play/HUD/KeyCounterController.cs b/osu.Game/Screens/Play/HUD/KeyCounterController.cs index b138e64d6f..0fa02afbb4 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterController.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterController.cs @@ -5,11 +5,8 @@ using System; using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; using osu.Game.Rulesets.UI; -using osuTK; namespace osu.Game.Screens.Play.HUD { @@ -17,8 +14,6 @@ namespace osu.Game.Screens.Play.HUD { public readonly Bindable IsCounting = new BindableBool(true); - private Receptor? receptor; - public event Action? OnNewTrigger; private readonly Container triggers; @@ -38,58 +33,8 @@ namespace osu.Game.Screens.Play.HUD } public void AddRange(IEnumerable inputTriggers) => inputTriggers.ForEach(Add); + public override bool HandleNonPositionalInput => true; - /// - /// Sets a that will populate keybinding events to this . - /// - /// The receptor to set - /// When a is already active on this - public void SetReceptor(Receptor receptor) - { - if (this.receptor != null) - throw new InvalidOperationException("Cannot set a new receptor when one is already active"); - - this.receptor = receptor; - } - - /// - /// Clears any active - /// - public void ClearReceptor() - { - receptor = null; - } - - public override bool HandleNonPositionalInput => receptor == null; - - public override bool HandlePositionalInput => receptor == null; - - public partial class Receptor : Drawable - { - protected readonly KeyCounterController Target; - - public Receptor(KeyCounterController target) - { - RelativeSizeAxes = Axes.Both; - Depth = float.MinValue; - Target = target; - } - - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - - protected override bool Handle(UIEvent e) - { - switch (e) - { - case KeyDownEvent: - case KeyUpEvent: - case MouseDownEvent: - case MouseUpEvent: - return Target.TriggerEvent(e); - } - - return base.Handle(e); - } - } + public override bool HandlePositionalInput => true; } } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 15a0e0688b..21636ac04c 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -158,8 +158,8 @@ namespace osu.Game.Screens.Play Spacing = new Vector2(5) }, clicksPerSecondCalculator = new ClicksPerSecondCalculator(), - KeyCounter = new KeyCounterController() }; + KeyCounter = new KeyCounterController(); hideTargets = new List { mainComponents, rulesetComponents, topRightElements }; From f83a4f495291a3983c15604d60392fa630cf6e56 Mon Sep 17 00:00:00 2001 From: tsrk Date: Sun, 18 Jun 2023 22:57:21 +0200 Subject: [PATCH 0265/2100] refactor: tidy up attachement flow TODO: find better naming and improve XMLDocs --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 6 +- osu.Game/Rulesets/UI/IKeybindingListener.cs | 50 ++++++++++++ osu.Game/Rulesets/UI/RulesetInputManager.cs | 78 +++++++------------ .../ClicksPerSecondCalculator.cs | 19 ++++- .../Screens/Play/HUD/KeyCounterController.cs | 21 ++++- osu.Game/Screens/Play/HUDOverlay.cs | 12 +-- 6 files changed, 126 insertions(+), 60 deletions(-) create mode 100644 osu.Game/Rulesets/UI/IKeybindingListener.cs diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 4fa18f53a7..e0a1533c4b 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.UI /// Displays an interactive ruleset gameplay instance. /// /// The type of HitObject contained by this DrawableRuleset. - public abstract partial class DrawableRuleset : DrawableRuleset, IProvideCursor, ICanAttachHUDPieces + public abstract partial class DrawableRuleset : DrawableRuleset, IProvideCursor, IKeybindingEventsEmitter where TObject : HitObject { public override event Action NewResult; @@ -327,8 +327,8 @@ namespace osu.Game.Rulesets.UI /// The representing . public abstract DrawableHitObject CreateDrawableRepresentation(TObject h); - public void Attach(IAttachableSkinComponent skinComponent) => - (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(skinComponent); + public void Attach(IKeybindingListener skinComponent) => + (KeyBindingInputManager as IKeybindingEventsEmitter)?.Attach(skinComponent); /// /// Creates a key conversion input manager. An exception will be thrown if a valid is not returned. diff --git a/osu.Game/Rulesets/UI/IKeybindingListener.cs b/osu.Game/Rulesets/UI/IKeybindingListener.cs new file mode 100644 index 0000000000..f38ce8643e --- /dev/null +++ b/osu.Game/Rulesets/UI/IKeybindingListener.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable +using System.Collections.Generic; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; + +namespace osu.Game.Rulesets.UI +{ + /// + /// Listens to events emitted by an . + /// Alternative to for classes that need to not depend on type parameters. + /// + public interface IKeybindingListener + { + /// + /// This class or a member of this class can already handle keybindings. + /// Signals to the that and + /// don't necessarily need to be called. + /// + /// + /// This is usually true for s and s that need to + /// pass s events to children that can already handle them. + /// + public bool CanHandleKeybindings { get; } + + /// + /// Prepares this class to receive events. + /// + /// The list of possible actions that can occur. + /// The type actions, commonly enums. + public void Setup(IEnumerable actions) where T : struct; + + /// + /// Called when an action is pressed. + /// + /// The event containing information about the pressed action. + /// The type of binding, commonly enums. + public void OnPressed(KeyBindingPressEvent action) where T : struct; + + /// + /// Called when an action is released. + /// + /// The event containing information about the released action. + /// The type of binding, commonly enums. + public void OnReleased(KeyBindingReleaseEvent action) where T : struct; + } +} diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index d3bc381f72..44c1f00cf7 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -19,13 +19,11 @@ using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Input.Handlers; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.Play.HUD; -using osu.Game.Screens.Play.HUD.ClicksPerSecond; using static osu.Game.Input.Handlers.ReplayInputHandler; namespace osu.Game.Rulesets.UI { - public abstract partial class RulesetInputManager : PassThroughInputManager, ICanAttachHUDPieces, IHasReplayHandler, IHasRecordingHandler + public abstract partial class RulesetInputManager : PassThroughInputManager, IKeybindingEventsEmitter, IHasReplayHandler, IHasRecordingHandler where T : struct { protected override bool AllowRightClickFromLongTouch => false; @@ -66,6 +64,7 @@ namespace osu.Game.Rulesets.UI InternalChild = KeyBindingContainer = CreateKeyBindingContainer(ruleset, variant, unique) .WithChild(content = new Container { RelativeSizeAxes = Axes.Both }); + KeyBindingContainer.Add(actionListener = new ActionListener()); } [BackgroundDependencyLoader(true)] @@ -160,63 +159,47 @@ namespace osu.Game.Rulesets.UI #region Component attachement - public void Attach(IAttachableSkinComponent skinComponent) + private readonly ActionListener actionListener; + + public void Attach(IKeybindingListener skinComponent) { - switch (skinComponent) - { - case KeyCounterController keyCounterDisplay: - attachKeyCounter(keyCounterDisplay); - break; - - case ClicksPerSecondCalculator clicksPerSecondCalculator: - attachClicksPerSecond(clicksPerSecondCalculator); - break; - } - } - - #endregion - - #region Key Counter Attachment - - private void attachKeyCounter(KeyCounterController keyCounter) - { - KeyBindingContainer.Add(keyCounter); - - keyCounter.AddRange(KeyBindingContainer.DefaultKeyBindings + skinComponent.Setup(KeyBindingContainer.DefaultKeyBindings .Select(b => b.GetAction()) .Distinct() - .OrderBy(action => action) - .Select(action => new KeyCounterActionTrigger(action))); - } + .OrderBy(a => a)); - #endregion + if (skinComponent.CanHandleKeybindings && skinComponent is Drawable component) + { + try + { + KeyBindingContainer.Add(component); + return; + } + catch (Exception) + { + return; + } + } - #region Keys per second Counter Attachment - - private void attachClicksPerSecond(ClicksPerSecondCalculator calculator) - { - var listener = new ActionListener(calculator); - - KeyBindingContainer.Add(listener); + actionListener.OnPressedEvent += skinComponent.OnPressed; + actionListener.OnReleasedEvent += skinComponent.OnReleased; } private partial class ActionListener : Component, IKeyBindingHandler { - private readonly ClicksPerSecondCalculator calculator; + public event Action> OnPressedEvent; - public ActionListener(ClicksPerSecondCalculator calculator) - { - this.calculator = calculator; - } + public event Action> OnReleasedEvent; public bool OnPressed(KeyBindingPressEvent e) { - calculator.AddInputTimestamp(); + OnPressedEvent?.Invoke(e); return false; } public void OnReleased(KeyBindingReleaseEvent e) { + OnReleasedEvent?.Invoke(e); } } @@ -257,16 +240,11 @@ namespace osu.Game.Rulesets.UI } /// - /// Supports attaching various HUD pieces. - /// Keys will be populated automatically and a receptor will be injected inside. + /// Sends events to a /// - public interface ICanAttachHUDPieces - { - void Attach(IAttachableSkinComponent component); - } - - public interface IAttachableSkinComponent + public interface IKeybindingEventsEmitter { + void Attach(IKeybindingListener component); } public class RulesetInputManagerInputState : InputState diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs index 3e55e11f1c..e0e93cb66e 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs @@ -4,11 +4,12 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Input.Events; using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Play.HUD.ClicksPerSecond { - public partial class ClicksPerSecondCalculator : Component, IAttachableSkinComponent + public partial class ClicksPerSecondCalculator : Component, IKeybindingListener { private readonly List timestamps = new List(); @@ -53,5 +54,21 @@ namespace osu.Game.Screens.Play.HUD.ClicksPerSecond Value = count; } + + #region IKeybindingListener + + bool IKeybindingListener.CanHandleKeybindings => false; + + void IKeybindingListener.Setup(IEnumerable actions) + { + } + + void IKeybindingListener.OnPressed(KeyBindingPressEvent action) => AddInputTimestamp(); + + void IKeybindingListener.OnReleased(KeyBindingReleaseEvent action) + { + } + + #endregion } } diff --git a/osu.Game/Screens/Play/HUD/KeyCounterController.cs b/osu.Game/Screens/Play/HUD/KeyCounterController.cs index 0fa02afbb4..2e678e55fc 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterController.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterController.cs @@ -3,14 +3,16 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Play.HUD { - public partial class KeyCounterController : CompositeComponent, IAttachableSkinComponent + public partial class KeyCounterController : CompositeComponent, IKeybindingListener { public readonly Bindable IsCounting = new BindableBool(true); @@ -36,5 +38,22 @@ namespace osu.Game.Screens.Play.HUD public override bool HandleNonPositionalInput => true; public override bool HandlePositionalInput => true; + + #region IKeybindingListener + + bool IKeybindingListener.CanHandleKeybindings => true; + + void IKeybindingListener.Setup(IEnumerable actions) + => AddRange(actions.Select(a => new KeyCounterActionTrigger(a))); + + void IKeybindingListener.OnPressed(KeyBindingPressEvent action) + { + } + + void IKeybindingListener.OnReleased(KeyBindingReleaseEvent action) + { + } + + #endregion } } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 21636ac04c..b74b5d835a 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -10,6 +10,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; @@ -102,6 +103,8 @@ namespace osu.Game.Screens.Play private readonly List hideTargets; + private readonly IEnumerable actionInjectionCandidates; + public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList mods, bool alwaysShowLeaderboard = true) { Drawable rulesetComponents; @@ -163,6 +166,8 @@ namespace osu.Game.Screens.Play hideTargets = new List { mainComponents, rulesetComponents, topRightElements }; + actionInjectionCandidates = new IKeybindingListener[] { clicksPerSecondCalculator, KeyCounter }; + if (!alwaysShowLeaderboard) hideTargets.Add(LeaderboardFlow); } @@ -319,11 +324,8 @@ namespace osu.Game.Screens.Play protected virtual void BindDrawableRuleset(DrawableRuleset drawableRuleset) { - if (drawableRuleset is ICanAttachHUDPieces attachTarget) - { - attachTarget.Attach(KeyCounter); - attachTarget.Attach(clicksPerSecondCalculator); - } + if (drawableRuleset is IKeybindingEventsEmitter attachTarget) + actionInjectionCandidates.ForEach(attachTarget.Attach); replayLoaded.BindTo(drawableRuleset.HasReplayLoaded); } From 0900cebc0dc03bd720ff11b5058fe68aa793d0af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Jun 2023 20:09:53 +0900 Subject: [PATCH 0266/2100] Avoid doing expensive colour fetch operation every update --- osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs index 2aec416867..6918993696 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs @@ -7,6 +7,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Screens.Edit; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Mods { @@ -26,14 +27,20 @@ namespace osu.Game.Rulesets.Osu.Mods currentBeatmap = beatmap; } - public void ApplyToDrawableHitObject(DrawableHitObject drawable) + public void ApplyToDrawableHitObject(DrawableHitObject d) { if (currentBeatmap == null) return; - drawable.OnUpdate += _ => - drawable.AccentColour.Value = BindableBeatDivisor.GetColourFor( - currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(drawable.HitObject.StartTime), - colours); + Color4? timingBasedColour = null; + + d.HitObjectApplied += _ => timingBasedColour = BindableBeatDivisor.GetColourFor(currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(d.HitObject.StartTime), colours); + + // Need to set this every update to ensure it doesn't get overwritten by DrawableHitObject.OnApply() -> UpdateComboColour(). + d.OnUpdate += _ => + { + if (timingBasedColour != null) + d.AccentColour.Value = timingBasedColour.Value; + }; } } } From 84fc6e92db99d725c24e7e4e6412d629a36a099b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Jun 2023 20:23:46 +0900 Subject: [PATCH 0267/2100] Fix slightly incorrect calculations --- .../Edit/Compose/Components/CircularDistanceSnapGrid.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index cf8b0c14ed..602ed6f627 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -65,14 +65,14 @@ namespace osu.Game.Screens.Edit.Compose.Components for (int i = 0; i < requiredCircles; i++) { - float diameter = (offset + (i + 1) * DistanceBetweenTicks) * 2; const float thickness = 4; + float diameter = (offset + (i + 1) * DistanceBetweenTicks + thickness / 2) * 2; AddInternal(new Ring(ReferenceObject, GetColourForIndexFromPlacement(i)) { Position = StartPosition, Origin = Anchor.Centre, - Size = new Vector2(diameter + thickness / 2), + Size = new Vector2(diameter), InnerRadius = thickness * 1f / diameter, }); } From 69526f25bb9743bb3fdca14a4c105bbd9d8465b7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Jun 2023 20:43:33 +0900 Subject: [PATCH 0268/2100] Add hotkey to save replay Defaults to `F2` aka stable. --- .../Input/Bindings/GlobalActionContainer.cs | 4 ++++ .../GlobalActionKeyBindingStrings.cs | 5 +++++ .../Screens/Play/SaveFailedScoreButton.cs | 21 ++++++++++++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index fdd96d3890..0ae29ebc8e 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -119,6 +119,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), new KeyBinding(InputKey.Tab, GlobalAction.ToggleChatFocus), + new KeyBinding(InputKey.F2, GlobalAction.SaveReplay), }; public IEnumerable ReplayKeyBindings => new[] @@ -366,5 +367,8 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleNextBeatSnapDivisor))] EditorCycleNextBeatSnapDivisor, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SaveReplay))] + SaveReplay, } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index aa608a603b..708fdaa174 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -324,6 +324,11 @@ namespace osu.Game.Localisation /// public static LocalisableString ToggleChatFocus => new TranslatableString(getKey(@"toggle_chat_focus"), @"Toggle chat focus"); + /// + /// "Save replay" + /// + public static LocalisableString SaveReplay => new TranslatableString(getKey(@"save_replay"), @"Save replay"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Play/SaveFailedScoreButton.cs b/osu.Game/Screens/Play/SaveFailedScoreButton.cs index 20d2130e76..c5bb265dcb 100644 --- a/osu.Game/Screens/Play/SaveFailedScoreButton.cs +++ b/osu.Game/Screens/Play/SaveFailedScoreButton.cs @@ -8,15 +8,18 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Game.Database; using osu.Game.Scoring; using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; using osu.Game.Online; using osuTK; namespace osu.Game.Screens.Play { - public partial class SaveFailedScoreButton : CompositeDrawable + public partial class SaveFailedScoreButton : CompositeDrawable, IKeyBindingHandler { private readonly Bindable state = new Bindable(); @@ -87,5 +90,21 @@ namespace osu.Game.Screens.Play } }, true); } + + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.SaveReplay: + button.TriggerClick(); + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } } } From bfa449e47ae959febca4c73c6edb035aabc99bc9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 19 Jun 2023 21:38:13 +0900 Subject: [PATCH 0269/2100] Adjust attribute data --- .../Difficulty/CatchDifficultyCalculator.cs | 4 +-- .../Difficulty/CatchScoreV1Processor.cs | 31 +++++++++++------- .../Difficulty/ManiaDifficultyCalculator.cs | 5 --- .../Difficulty/OsuDifficultyCalculator.cs | 4 +-- .../Difficulty/OsuScoreV1Processor.cs | 32 ++++++++++++------- .../Difficulty/TaikoDifficultyCalculator.cs | 4 +-- .../Difficulty/TaikoScoreV1Processor.cs | 32 ++++++++++++------- .../Difficulty/DifficultyAttributes.cs | 27 ++++++++-------- 8 files changed, 82 insertions(+), 57 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index fb7c4f05f4..36af9fb980 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -49,9 +49,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty Mods = mods, ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)), - LegacyTotalScore = sv1Processor.TotalScore, + LegacyAccuracyScore = sv1Processor.AccuracyScore, LegacyComboScore = sv1Processor.ComboScore, - LegacyBonusScore = sv1Processor.BonusScore + LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio }; } diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs index b5c3838fdc..3f0ac7a760 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs @@ -6,31 +6,34 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Difficulty { internal class CatchScoreV1Processor { - public int TotalScore => BaseScore + ComboScore + BonusScore; + /// + /// The accuracy portion of the legacy (ScoreV1) total score. + /// + public int AccuracyScore { get; private set; } /// - /// Amount of score that is combo-and-difficulty-multiplied, excluding mod multipliers. + /// The combo-multiplied portion of the legacy (ScoreV1) total score. /// public int ComboScore { get; private set; } /// - /// Amount of score that is NOT combo-and-difficulty-multiplied. + /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. + /// This is made up of all judgements that would be or . /// - public int BaseScore { get; private set; } - - /// - /// Amount of score whose judgements would be treated as "bonus" in ScoreV2. - /// - public int BonusScore { get; private set; } + public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore; + private int legacyBonusScore; + private int modernBonusScore; private int combo; private readonly double scoreMultiplier; @@ -77,7 +80,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty { bool increaseCombo = true; bool addScoreComboMultiplier = false; + bool isBonus = false; + HitResult bonusResult = HitResult.None; int scoreIncrease = 0; @@ -102,6 +107,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty scoreIncrease = 1100; increaseCombo = false; isBonus = true; + bonusResult = HitResult.LargeBonus; break; case JuiceStream: @@ -122,9 +128,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty } if (isBonus) - BonusScore += scoreIncrease; + { + legacyBonusScore += scoreIncrease; + modernBonusScore += Judgement.ToNumericResult(bonusResult); + } else - BaseScore += scoreIncrease; + AccuracyScore += scoreIncrease; if (increaseCombo) combo++; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index cb41b93deb..d1058a9f8c 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -33,13 +33,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty public override int Version => 20220902; - private readonly IWorkingBeatmap workingBeatmap; - public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { - workingBeatmap = beatmap; - isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset); originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty; } @@ -62,7 +58,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being. GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate), MaxCombo = beatmap.HitObjects.Sum(maxComboForObject), - LegacyTotalScore = sv1Processor.TotalScore, LegacyComboScore = sv1Processor.TotalScore }; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 21ee03d1a5..5d6ed4792d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -109,9 +109,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty HitCircleCount = hitCirclesCount, SliderCount = sliderCount, SpinnerCount = spinnerCount, - LegacyTotalScore = sv1Processor.TotalScore, + LegacyAccuracyScore = sv1Processor.AccuracyScore, LegacyComboScore = sv1Processor.ComboScore, - LegacyBonusScore = sv1Processor.BonusScore + LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio }; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs index c82928b745..28d029b73a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs @@ -5,32 +5,35 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Difficulty { internal class OsuScoreV1Processor { - public int TotalScore => BaseScore + ComboScore + BonusScore; + /// + /// The accuracy portion of the legacy (ScoreV1) total score. + /// + public int AccuracyScore { get; private set; } /// - /// Amount of score that is combo-and-difficulty-multiplied, excluding mod multipliers. + /// The combo-multiplied portion of the legacy (ScoreV1) total score. /// public int ComboScore { get; private set; } /// - /// Amount of score that is NOT combo-and-difficulty-multiplied. + /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. + /// This is made up of all judgements that would be or . /// - public int BaseScore { get; private set; } - - /// - /// Amount of score whose judgements would be treated as "bonus" in ScoreV2. - /// - public int BonusScore { get; private set; } + public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore; + private int legacyBonusScore; + private int modernBonusScore; private int combo; private readonly double scoreMultiplier; @@ -80,7 +83,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty { bool increaseCombo = true; bool addScoreComboMultiplier = false; + bool isBonus = false; + HitResult bonusResult = HitResult.None; int scoreIncrease = 0; @@ -100,12 +105,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty scoreIncrease = 1100; increaseCombo = false; isBonus = true; + bonusResult = HitResult.LargeBonus; break; case SpinnerTick: scoreIncrease = 100; increaseCombo = false; isBonus = true; + bonusResult = HitResult.SmallBonus; break; case HitCircle: @@ -156,9 +163,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty } if (isBonus) - BonusScore += scoreIncrease; + { + legacyBonusScore += scoreIncrease; + modernBonusScore += Judgement.ToNumericResult(bonusResult); + } else - BaseScore += scoreIncrease; + AccuracyScore += scoreIncrease; if (increaseCombo) combo++; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 28b07c0d59..49222adc89 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -101,9 +101,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty PeakDifficulty = combinedRating, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), - LegacyTotalScore = sv1Processor.TotalScore, + LegacyAccuracyScore = sv1Processor.AccuracyScore, LegacyComboScore = sv1Processor.ComboScore, - LegacyBonusScore = sv1Processor.BonusScore + LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio }; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs index ee52424b26..23ff9585e8 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs @@ -5,32 +5,35 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty { internal class TaikoScoreV1Processor { - public int TotalScore => BaseScore + ComboScore + BonusScore; + /// + /// The accuracy portion of the legacy (ScoreV1) total score. + /// + public int AccuracyScore { get; private set; } /// - /// Amount of score that is combo-and-difficulty-multiplied, excluding mod multipliers. + /// The combo-multiplied portion of the legacy (ScoreV1) total score. /// public int ComboScore { get; private set; } /// - /// Amount of score that is NOT combo-and-difficulty-multiplied. + /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. + /// This is made up of all judgements that would be or . /// - public int BaseScore { get; private set; } - - /// - /// Amount of score whose judgements would be treated as "bonus" in ScoreV2. - /// - public int BonusScore { get; private set; } + public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore; + private int legacyBonusScore; + private int modernBonusScore; private int combo; private readonly double modMultiplier; @@ -83,7 +86,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { bool increaseCombo = true; bool addScoreComboMultiplier = false; + bool isBonus = false; + HitResult bonusResult = HitResult.None; int scoreIncrease = 0; @@ -98,6 +103,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty scoreIncrease = 300; increaseCombo = false; isBonus = true; + bonusResult = HitResult.SmallBonus; break; case Swell swell: @@ -123,6 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty addScoreComboMultiplier = true; increaseCombo = false; isBonus = true; + bonusResult = HitResult.LargeBonus; break; case Hit: @@ -181,9 +188,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty ComboScore += comboScoreIncrease; if (isBonus) - BonusScore += scoreIncrease; + { + legacyBonusScore += scoreIncrease; + modernBonusScore += Judgement.ToNumericResult(bonusResult); + } else - BaseScore += scoreIncrease; + AccuracyScore += scoreIncrease; if (increaseCombo) combo++; diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 5a51fb24a6..48e67ff425 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -27,9 +27,9 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_FLASHLIGHT = 17; protected const int ATTRIB_ID_SLIDER_FACTOR = 19; protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21; - protected const int ATTRIB_ID_LEGACY_TOTAL_SCORE = 23; + protected const int ATTRIB_ID_LEGACY_ACCURACY_SCORE = 23; protected const int ATTRIB_ID_LEGACY_COMBO_SCORE = 25; - protected const int ATTRIB_ID_LEGACY_BONUS_SCORE = 27; + protected const int ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO = 27; /// /// The mods which were applied to the beatmap. @@ -49,22 +49,23 @@ namespace osu.Game.Rulesets.Difficulty public int MaxCombo { get; set; } /// - /// The maximum achievable legacy total score. + /// The accuracy portion of the legacy (ScoreV1) total score. /// - [JsonProperty("legacy_total_score", Order = -5)] - public int LegacyTotalScore { get; set; } + [JsonProperty("legacy_accuracy_score", Order = -5)] + public int LegacyAccuracyScore { get; set; } /// - /// The combo-multiplied portion of . + /// The combo-multiplied portion of the legacy (ScoreV1) total score. /// [JsonProperty("legacy_combo_score", Order = -4)] public int LegacyComboScore { get; set; } /// - /// The "bonus" portion of consisting of all judgements that would be or . + /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. + /// This is made up of all judgements that would be or . /// - [JsonProperty("legacy_bonus_score", Order = -3)] - public int LegacyBonusScore { get; set; } + [JsonProperty("legacy_bonus_score_ratio", Order = -3)] + public double LegacyBonusScoreRatio { get; set; } /// /// Creates new . @@ -93,9 +94,9 @@ namespace osu.Game.Rulesets.Difficulty public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); - yield return (ATTRIB_ID_LEGACY_TOTAL_SCORE, LegacyTotalScore); + yield return (ATTRIB_ID_LEGACY_ACCURACY_SCORE, LegacyAccuracyScore); yield return (ATTRIB_ID_LEGACY_COMBO_SCORE, LegacyComboScore); - yield return (ATTRIB_ID_LEGACY_BONUS_SCORE, LegacyBonusScore); + yield return (ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO, LegacyBonusScoreRatio); } /// @@ -106,9 +107,9 @@ namespace osu.Game.Rulesets.Difficulty public virtual void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; - LegacyTotalScore = (int)values[ATTRIB_ID_LEGACY_TOTAL_SCORE]; + LegacyAccuracyScore = (int)values[ATTRIB_ID_LEGACY_ACCURACY_SCORE]; LegacyComboScore = (int)values[ATTRIB_ID_LEGACY_COMBO_SCORE]; - LegacyBonusScore = (int)values[ATTRIB_ID_LEGACY_BONUS_SCORE]; + LegacyBonusScoreRatio = (int)values[ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO]; } } } From 6d32206a08e0f854fa67ef751c1032679072938f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 19 Jun 2023 17:47:01 +0200 Subject: [PATCH 0270/2100] Fix slider tails receiving wrong colours Only visually apparent on legacy skins. --- osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs index 6918993696..9537f8b388 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs @@ -6,6 +6,7 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Screens.Edit; using osuTK.Graphics; @@ -33,7 +34,15 @@ namespace osu.Game.Rulesets.Osu.Mods Color4? timingBasedColour = null; - d.HitObjectApplied += _ => timingBasedColour = BindableBeatDivisor.GetColourFor(currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(d.HitObject.StartTime), colours); + d.HitObjectApplied += _ => + { + // slider tails are a painful edge case, as their start time is offset 36ms back (see `LegacyLastTick`). + // to work around this, look up the slider tail's parenting slider's end time instead to ensure proper snap. + double snapTime = d is DrawableSliderTail tail + ? tail.Slider.GetEndTime() + : d.HitObject.StartTime; + timingBasedColour = BindableBeatDivisor.GetColourFor(currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(snapTime), colours); + }; // Need to set this every update to ensure it doesn't get overwritten by DrawableHitObject.OnApply() -> UpdateComboColour(). d.OnUpdate += _ => From 9bcd86d66d8514319ce75f14bbd7362eb1a50688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 19 Jun 2023 18:42:30 +0200 Subject: [PATCH 0271/2100] Fix test failure due to relying on implementation detail --- .../Editor/TestSceneOsuDistanceSnapGrid.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index 7579e8077b..0c064ecfa6 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -185,7 +185,18 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("Ensure cursor is on a grid line", () => { - return grid.ChildrenOfType().Any(p => Precision.AlmostEquals(p.ScreenSpaceDrawQuad.TopRight.X, grid.ToScreenSpace(cursor.LastSnappedPosition).X)); + return grid.ChildrenOfType().Any(ring => + { + // the grid rings are actually slightly _larger_ than the snapping radii. + // this is done such that the snapping radius falls right in the middle of each grid ring thickness-wise, + // but it does however complicate the following calculations slightly. + + // we want to calculate the coordinates of the rightmost point on the grid line, which is in the exact middle of the ring thickness-wise. + // for the X component, we take the entire width of the ring, minus one half of the inner radius (since we want the middle of the line on the right side). + // for the Y component, we just take 0.5f. + var rightMiddleOfGridLine = ring.ToScreenSpace(ring.DrawSize * new Vector2(1 - ring.InnerRadius / 2, 0.5f)); + return Precision.AlmostEquals(rightMiddleOfGridLine.X, grid.ToScreenSpace(cursor.LastSnappedPosition).X); + }); }); } From 362fe62b4bbea9b590614b6df05587ad602fb2d0 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 19 Jun 2023 10:20:56 -0700 Subject: [PATCH 0272/2100] Fix beatmap info not showing individual difficulty bpm --- osu.Game/Overlays/BeatmapSet/BasicStats.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs index 3cc655d561..0b1befe7b9 100644 --- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs +++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs @@ -58,16 +58,18 @@ namespace osu.Game.Overlays.BeatmapSet private void updateDisplay() { - bpm.Value = BeatmapSet?.BPM.ToLocalisableString(@"0.##") ?? (LocalisableString)"-"; - if (beatmapInfo == null) { + bpm.Value = "-"; + length.Value = string.Empty; circleCount.Value = string.Empty; sliderCount.Value = string.Empty; } else { + bpm.Value = beatmapInfo.BPM.ToLocalisableString(@"0.##"); + length.Value = TimeSpan.FromMilliseconds(beatmapInfo.Length).ToFormattedDuration(); if (beatmapInfo is not IBeatmapOnlineInfo onlineInfo) return; From 4a9543092a663a7695368c4fadb691c174a2a3d4 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Mon, 19 Jun 2023 17:08:04 -0400 Subject: [PATCH 0273/2100] disable posting comments when logged out --- osu.Game/Overlays/Comments/CommentEditor.cs | 25 +++++++++++++++++-- .../Overlays/Comments/ReplyCommentEditor.cs | 3 ++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index 2af7dd3093..5c9f78d05f 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -13,6 +13,9 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; @@ -26,6 +29,8 @@ namespace osu.Game.Overlays.Comments protected abstract LocalisableString CommitButtonText { get; } + private LocalisableString textBoxPlaceholderLoggedOut => AuthorizationStrings.RequireLogin; + protected abstract LocalisableString TextBoxPlaceholder { get; } protected FillFlowContainer ButtonsContainer { get; private set; } = null!; @@ -37,6 +42,13 @@ namespace osu.Game.Overlays.Comments protected TextBox TextBox { get; private set; } = null!; + protected readonly IBindable User = new Bindable(); + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private LocalisableString placeholderText => api.IsLoggedIn ? TextBoxPlaceholder : textBoxPlaceholderLoggedOut; + protected bool ShowLoadingSpinner { set @@ -78,8 +90,9 @@ namespace osu.Game.Overlays.Comments { Height = 40, RelativeSizeAxes = Axes.X, - PlaceholderText = TextBoxPlaceholder, - Current = Current + PlaceholderText = placeholderText, + Current = Current, + ReadOnly = !api.IsLoggedIn }, new Container { @@ -134,12 +147,14 @@ namespace osu.Game.Overlays.Comments }); TextBox.OnCommit += (_, _) => commitButton.TriggerClick(); + User.BindTo(api.LocalUser); } protected override void LoadComplete() { base.LoadComplete(); Current.BindValueChanged(_ => updateCommitButtonState(), true); + User.BindValueChanged(_ => updateTextBoxState()); } protected abstract void OnCommit(string text); @@ -147,6 +162,12 @@ namespace osu.Game.Overlays.Comments private void updateCommitButtonState() => commitButton.Enabled.Value = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value); + private void updateTextBoxState() + { + TextBox.PlaceholderText = placeholderText; + TextBox.ReadOnly = !api.IsLoggedIn; + } + private partial class EditorTextBox : OsuTextBox { protected override float LeftRightPadding => side_padding; diff --git a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs index 8aca183dee..8c4b25a7dc 100644 --- a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs +++ b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs @@ -38,7 +38,8 @@ namespace osu.Game.Overlays.Comments { base.LoadComplete(); - GetContainingInputManager().ChangeFocus(TextBox); + if (!TextBox.ReadOnly) + GetContainingInputManager().ChangeFocus(TextBox); } protected override void OnCommit(string text) From d5d494f07bd7dd97682214f7b386ebfba9ff3655 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Mon, 19 Jun 2023 17:36:40 -0400 Subject: [PATCH 0274/2100] resolve protected API in comments superclass --- osu.Game/Overlays/Comments/CommentEditor.cs | 10 +++++----- osu.Game/Overlays/Comments/CommentsContainer.cs | 5 +---- osu.Game/Overlays/Comments/ReplyCommentEditor.cs | 6 +----- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index 5c9f78d05f..35c96cf531 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -45,9 +45,9 @@ namespace osu.Game.Overlays.Comments protected readonly IBindable User = new Bindable(); [Resolved] - private IAPIProvider api { get; set; } = null!; + protected IAPIProvider API { get; private set; } = null!; - private LocalisableString placeholderText => api.IsLoggedIn ? TextBoxPlaceholder : textBoxPlaceholderLoggedOut; + private LocalisableString placeholderText => API.IsLoggedIn ? TextBoxPlaceholder : textBoxPlaceholderLoggedOut; protected bool ShowLoadingSpinner { @@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Comments RelativeSizeAxes = Axes.X, PlaceholderText = placeholderText, Current = Current, - ReadOnly = !api.IsLoggedIn + ReadOnly = !API.IsLoggedIn }, new Container { @@ -147,7 +147,7 @@ namespace osu.Game.Overlays.Comments }); TextBox.OnCommit += (_, _) => commitButton.TriggerClick(); - User.BindTo(api.LocalUser); + User.BindTo(API.LocalUser); } protected override void LoadComplete() @@ -165,7 +165,7 @@ namespace osu.Game.Overlays.Comments private void updateTextBoxState() { TextBox.PlaceholderText = placeholderText; - TextBox.ReadOnly = !api.IsLoggedIn; + TextBox.ReadOnly = !API.IsLoggedIn; } private partial class EditorTextBox : OsuTextBox diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index 24536fe460..f50bbb7116 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -405,9 +405,6 @@ namespace osu.Game.Overlays.Comments [Resolved] private CommentsContainer commentsContainer { get; set; } - [Resolved] - private IAPIProvider api { get; set; } - public Action OnPost; //TODO should match web, left empty due to no multiline support @@ -432,7 +429,7 @@ namespace osu.Game.Overlays.Comments Current.Value = string.Empty; OnPost?.Invoke(cb); }); - api.Queue(req); + API.Queue(req); } } } diff --git a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs index 8c4b25a7dc..7fbf556e1f 100644 --- a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs +++ b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs @@ -6,7 +6,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Localisation; using osu.Framework.Logging; -using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; @@ -18,9 +17,6 @@ namespace osu.Game.Overlays.Comments [Resolved] private CommentsContainer commentsContainer { get; set; } = null!; - [Resolved] - private IAPIProvider api { get; set; } = null!; - private readonly Comment parentComment; public Action? OnPost; @@ -52,7 +48,7 @@ namespace osu.Game.Overlays.Comments Logger.Error(e, "Posting reply comment failed."); }); req.Success += cb => Schedule(processPostedComments, cb); - api.Queue(req); + API.Queue(req); } private void processPostedComments(CommentBundle cb) From 591277e0f97bc3e2e63219d23c24d38b28daa0eb Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Mon, 19 Jun 2023 18:10:37 -0400 Subject: [PATCH 0275/2100] extract button text properties to methods, show login overlay on click --- osu.Game/Overlays/Comments/CommentEditor.cs | 48 ++++++++++++------- .../Overlays/Comments/CommentsContainer.cs | 6 ++- .../Overlays/Comments/ReplyCommentEditor.cs | 8 +++- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index 35c96cf531..58fba4bb57 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -15,7 +15,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; @@ -27,12 +26,6 @@ namespace osu.Game.Overlays.Comments protected abstract LocalisableString FooterText { get; } - protected abstract LocalisableString CommitButtonText { get; } - - private LocalisableString textBoxPlaceholderLoggedOut => AuthorizationStrings.RequireLogin; - - protected abstract LocalisableString TextBoxPlaceholder { get; } - protected FillFlowContainer ButtonsContainer { get; private set; } = null!; protected readonly Bindable Current = new Bindable(string.Empty); @@ -47,7 +40,12 @@ namespace osu.Game.Overlays.Comments [Resolved] protected IAPIProvider API { get; private set; } = null!; - private LocalisableString placeholderText => API.IsLoggedIn ? TextBoxPlaceholder : textBoxPlaceholderLoggedOut; + [Resolved] + private LoginOverlay? loginOverlay { get; set; } + + protected abstract LocalisableString GetCommitButtonText(bool isLoggedIn); + + protected abstract LocalisableString GetTextBoxPlaceholder(bool isLoggedIn); protected bool ShowLoadingSpinner { @@ -90,7 +88,7 @@ namespace osu.Game.Overlays.Comments { Height = 40, RelativeSizeAxes = Axes.X, - PlaceholderText = placeholderText, + PlaceholderText = GetTextBoxPlaceholder(API.IsLoggedIn), Current = Current, ReadOnly = !API.IsLoggedIn }, @@ -128,8 +126,8 @@ namespace osu.Game.Overlays.Comments Spacing = new Vector2(5, 0), Child = commitButton = new EditorButton { - Text = CommitButtonText, - Action = () => OnCommit(Current.Value) + Text = GetCommitButtonText(API.IsLoggedIn), + Action = () => commitOrLogIn(Current.Value) } }, loadingSpinner = new LoadingSpinner @@ -154,18 +152,34 @@ namespace osu.Game.Overlays.Comments { base.LoadComplete(); Current.BindValueChanged(_ => updateCommitButtonState(), true); - User.BindValueChanged(_ => updateTextBoxState()); + User.BindValueChanged(_ => updateStateForLoggedIn()); } protected abstract void OnCommit(string text); - private void updateCommitButtonState() => - commitButton.Enabled.Value = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value); - - private void updateTextBoxState() + private void commitOrLogIn(string text) { - TextBox.PlaceholderText = placeholderText; + if (!API.IsLoggedIn) + { + loginOverlay?.Show(); + return; + } + + OnCommit(text); + } + + private void updateCommitButtonState() + { + bool textBoxValid = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value); + commitButton.Enabled.Value = textBoxValid || !API.IsLoggedIn; + } + + private void updateStateForLoggedIn() + { + TextBox.PlaceholderText = GetTextBoxPlaceholder(API.IsLoggedIn); TextBox.ReadOnly = !API.IsLoggedIn; + commitButton.Text = GetCommitButtonText(API.IsLoggedIn); + updateCommitButtonState(); } private partial class EditorTextBox : OsuTextBox diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index f50bbb7116..1dfcb5f5c6 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -410,9 +410,11 @@ namespace osu.Game.Overlays.Comments //TODO should match web, left empty due to no multiline support protected override LocalisableString FooterText => default; - protected override LocalisableString CommitButtonText => CommonStrings.ButtonsPost; + protected override LocalisableString GetCommitButtonText(bool isLoggedIn) => + isLoggedIn ? CommonStrings.ButtonsPost : CommentsStrings.GuestButtonNew; - protected override LocalisableString TextBoxPlaceholder => CommentsStrings.PlaceholderNew; + protected override LocalisableString GetTextBoxPlaceholder(bool isLoggedIn) => + isLoggedIn ? CommentsStrings.PlaceholderNew : AuthorizationStrings.RequireLogin; protected override void OnCommit(string text) { diff --git a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs index 7fbf556e1f..52e301f0a3 100644 --- a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs +++ b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs @@ -22,8 +22,12 @@ namespace osu.Game.Overlays.Comments public Action? OnPost; protected override LocalisableString FooterText => default; - protected override LocalisableString CommitButtonText => CommonStrings.ButtonsReply; - protected override LocalisableString TextBoxPlaceholder => CommentsStrings.PlaceholderReply; + + protected override LocalisableString GetCommitButtonText(bool isLoggedIn) => + isLoggedIn ? CommonStrings.ButtonsReply : CommentsStrings.GuestButtonReply; + + protected override LocalisableString GetTextBoxPlaceholder(bool isLoggedIn) => + isLoggedIn ? CommentsStrings.PlaceholderReply : AuthorizationStrings.RequireLogin; public ReplyCommentEditor(Comment parent) { From f7dde53f9b11afc256c0bb6103be4904ff0a84df Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Mon, 19 Jun 2023 18:20:16 -0400 Subject: [PATCH 0276/2100] use runOnceImmediately instead of duplicating logic --- osu.Game/Overlays/Comments/CommentEditor.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index 58fba4bb57..b139fbefe6 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -88,9 +88,7 @@ namespace osu.Game.Overlays.Comments { Height = 40, RelativeSizeAxes = Axes.X, - PlaceholderText = GetTextBoxPlaceholder(API.IsLoggedIn), - Current = Current, - ReadOnly = !API.IsLoggedIn + Current = Current }, new Container { @@ -126,7 +124,6 @@ namespace osu.Game.Overlays.Comments Spacing = new Vector2(5, 0), Child = commitButton = new EditorButton { - Text = GetCommitButtonText(API.IsLoggedIn), Action = () => commitOrLogIn(Current.Value) } }, @@ -152,7 +149,7 @@ namespace osu.Game.Overlays.Comments { base.LoadComplete(); Current.BindValueChanged(_ => updateCommitButtonState(), true); - User.BindValueChanged(_ => updateStateForLoggedIn()); + User.BindValueChanged(_ => updateStateForLoggedIn(), true); } protected abstract void OnCommit(string text); From 60eedbafd1be892a0e248eabb955090eff6eaf48 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Mon, 19 Jun 2023 22:05:18 -0400 Subject: [PATCH 0277/2100] rename GetTextBoxPlaceholder to GetPlaceholderText --- osu.Game/Overlays/Comments/CommentEditor.cs | 4 ++-- osu.Game/Overlays/Comments/CommentsContainer.cs | 2 +- osu.Game/Overlays/Comments/ReplyCommentEditor.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index b139fbefe6..b363fe5881 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -45,7 +45,7 @@ namespace osu.Game.Overlays.Comments protected abstract LocalisableString GetCommitButtonText(bool isLoggedIn); - protected abstract LocalisableString GetTextBoxPlaceholder(bool isLoggedIn); + protected abstract LocalisableString GetPlaceholderText(bool isLoggedIn); protected bool ShowLoadingSpinner { @@ -173,7 +173,7 @@ namespace osu.Game.Overlays.Comments private void updateStateForLoggedIn() { - TextBox.PlaceholderText = GetTextBoxPlaceholder(API.IsLoggedIn); + TextBox.PlaceholderText = GetPlaceholderText(API.IsLoggedIn); TextBox.ReadOnly = !API.IsLoggedIn; commitButton.Text = GetCommitButtonText(API.IsLoggedIn); updateCommitButtonState(); diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index 1dfcb5f5c6..e6c69d2090 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -413,7 +413,7 @@ namespace osu.Game.Overlays.Comments protected override LocalisableString GetCommitButtonText(bool isLoggedIn) => isLoggedIn ? CommonStrings.ButtonsPost : CommentsStrings.GuestButtonNew; - protected override LocalisableString GetTextBoxPlaceholder(bool isLoggedIn) => + protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => isLoggedIn ? CommentsStrings.PlaceholderNew : AuthorizationStrings.RequireLogin; protected override void OnCommit(string text) diff --git a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs index 52e301f0a3..0d210021b9 100644 --- a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs +++ b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs @@ -26,7 +26,7 @@ namespace osu.Game.Overlays.Comments protected override LocalisableString GetCommitButtonText(bool isLoggedIn) => isLoggedIn ? CommonStrings.ButtonsReply : CommentsStrings.GuestButtonReply; - protected override LocalisableString GetTextBoxPlaceholder(bool isLoggedIn) => + protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => isLoggedIn ? CommentsStrings.PlaceholderReply : AuthorizationStrings.RequireLogin; public ReplyCommentEditor(Comment parent) From 343052410b1eb83be437e1ed0d49343b474b1c37 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Mon, 19 Jun 2023 22:08:45 -0400 Subject: [PATCH 0278/2100] update CommentEditor test components --- .../Visual/UserInterface/TestSceneCommentEditor.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs index e7840d4a2a..0bc6367d64 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs @@ -125,8 +125,8 @@ namespace osu.Game.Tests.Visual.UserInterface } protected override LocalisableString FooterText => @"Footer text. And it is pretty long. Cool."; - protected override LocalisableString CommitButtonText => @"Commit"; - protected override LocalisableString TextBoxPlaceholder => @"This text box is empty"; + protected override LocalisableString GetCommitButtonText(bool isLoggedIn) => @"Commit"; + protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => @"This text box is empty"; } private partial class TestCancellableCommentEditor : CancellableCommentEditor @@ -146,8 +146,8 @@ namespace osu.Game.Tests.Visual.UserInterface { } - protected override LocalisableString CommitButtonText => @"Save"; - protected override LocalisableString TextBoxPlaceholder => @"Multiline textboxes soon"; + protected override LocalisableString GetCommitButtonText(bool isLoggedIn) => @"Save"; + protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => @"Multiline textboxes soon"; } } } From 1e0e29847f3404d5f90d52bc467b6d0cf7a55fd9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jun 2023 16:29:15 +0900 Subject: [PATCH 0279/2100] Apply NRT and hotkey support to save replay button at results screen --- .../Screens/Ranking/ReplayDownloadButton.cs | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index 5c5cb61b79..5a1e59403a 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -1,30 +1,31 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; using osu.Game.Online; using osu.Game.Scoring; using osuTK; namespace osu.Game.Screens.Ranking { - public partial class ReplayDownloadButton : CompositeDrawable + public partial class ReplayDownloadButton : CompositeDrawable, IKeyBindingHandler { public readonly Bindable Score = new Bindable(); protected readonly Bindable State = new Bindable(); - private DownloadButton button; - private ShakeContainer shakeContainer; + private DownloadButton button = null!; + private ShakeContainer shakeContainer = null!; - private ScoreDownloadTracker downloadTracker; + private ScoreDownloadTracker? downloadTracker; private ReplayAvailability replayAvailability { @@ -46,8 +47,8 @@ namespace osu.Game.Screens.Ranking Size = new Vector2(50, 30); } - [BackgroundDependencyLoader(true)] - private void load(OsuGame game, ScoreModelDownloader scores) + [BackgroundDependencyLoader] + private void load(OsuGame? game, ScoreModelDownloader scores) { InternalChild = shakeContainer = new ShakeContainer { @@ -99,6 +100,22 @@ namespace osu.Game.Screens.Ranking }, true); } + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.SaveReplay: + button.TriggerClick(); + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + private void updateState() { switch (replayAvailability) From 7c5813c05af98212a4e6e4eb85ce9ea27dcf5a91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jun 2023 16:34:22 +0900 Subject: [PATCH 0280/2100] Fix `OsuAnimatedButton` not flashing when triggered via code --- osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs index 5ef590d253..69e8df0286 100644 --- a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs @@ -111,6 +111,10 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnClick(ClickEvent e) { + // Handle case where a click is triggered via TriggerClick(). + if (!IsHovered) + hover.FadeOutFromOne(1600); + hover.FlashColour(FlashColour, 800, Easing.OutQuint); return base.OnClick(e); } From 4bd121d3b8eaf17b6df6bc9a8f19bb3c2ba11dfe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jun 2023 16:38:30 +0900 Subject: [PATCH 0281/2100] Also add hotkey to export replays --- .../Input/Bindings/GlobalActionContainer.cs | 6 ++++- .../GlobalActionKeyBindingStrings.cs | 5 ++++ .../Screens/Play/SaveFailedScoreButton.cs | 17 +++++++++++++- .../Screens/Ranking/ReplayDownloadButton.cs | 23 +++++++++++++++++-- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 0ae29ebc8e..64268c73d0 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -119,7 +119,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), new KeyBinding(InputKey.Tab, GlobalAction.ToggleChatFocus), - new KeyBinding(InputKey.F2, GlobalAction.SaveReplay), + new KeyBinding(InputKey.F1, GlobalAction.SaveReplay), + new KeyBinding(InputKey.F2, GlobalAction.ExportReplay), }; public IEnumerable ReplayKeyBindings => new[] @@ -370,5 +371,8 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SaveReplay))] SaveReplay, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ExportReplay))] + ExportReplay, } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 708fdaa174..9e53b23180 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -329,6 +329,11 @@ namespace osu.Game.Localisation /// public static LocalisableString SaveReplay => new TranslatableString(getKey(@"save_replay"), @"Save replay"); + /// + /// "Export replay" + /// + public static LocalisableString ExportReplay => new TranslatableString(getKey(@"export_replay"), @"Export replay"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Play/SaveFailedScoreButton.cs b/osu.Game/Screens/Play/SaveFailedScoreButton.cs index c5bb265dcb..dc0ac054cb 100644 --- a/osu.Game/Screens/Play/SaveFailedScoreButton.cs +++ b/osu.Game/Screens/Play/SaveFailedScoreButton.cs @@ -21,6 +21,12 @@ namespace osu.Game.Screens.Play { public partial class SaveFailedScoreButton : CompositeDrawable, IKeyBindingHandler { + [Resolved] + private RealmAccess realm { get; set; } = null!; + + [Resolved] + private ScoreManager scoreManager { get; set; } = null!; + private readonly Bindable state = new Bindable(); private readonly Func> importFailedScore; @@ -37,7 +43,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load(OsuGame? game, Player? player, RealmAccess realm) + private void load(OsuGame? game, Player? player) { InternalChild = button = new DownloadButton { @@ -98,6 +104,15 @@ namespace osu.Game.Screens.Play case GlobalAction.SaveReplay: button.TriggerClick(); return true; + + case GlobalAction.ExportReplay: + Task.Run(importFailedScore).ContinueWith(t => + { + importedScore = realm.Run(r => r.Find(t.GetResultSafely().ID)?.Detach()); + Schedule(() => state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded); + scoreManager.Export(importedScore); + }); + return true; } return false; diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index 5a1e59403a..0772f54860 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -27,6 +27,9 @@ namespace osu.Game.Screens.Ranking private ScoreDownloadTracker? downloadTracker; + [Resolved] + private ScoreManager scoreManager { get; set; } = null!; + private ReplayAvailability replayAvailability { get @@ -48,7 +51,7 @@ namespace osu.Game.Screens.Ranking } [BackgroundDependencyLoader] - private void load(OsuGame? game, ScoreModelDownloader scores) + private void load(OsuGame? game, ScoreModelDownloader scoreDownloader) { InternalChild = shakeContainer = new ShakeContainer { @@ -68,7 +71,7 @@ namespace osu.Game.Screens.Ranking break; case DownloadState.NotDownloaded: - scores.Download(Score.Value); + scoreDownloader.Download(Score.Value); break; case DownloadState.Importing: @@ -107,6 +110,22 @@ namespace osu.Game.Screens.Ranking case GlobalAction.SaveReplay: button.TriggerClick(); return true; + + case GlobalAction.ExportReplay: + if (State.Value == DownloadState.NotDownloaded) + { + button.TriggerClick(); + } + + State.ValueChanged += importAfterDownload; + + void importAfterDownload(ValueChangedEvent valueChangedEvent) + { + scoreManager.Export(Score.Value); + State.ValueChanged -= importAfterDownload; + } + + return true; } return false; From 7b69b92eab65b1692bd17b8a0796a796bdcf7cb4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jun 2023 16:56:41 +0900 Subject: [PATCH 0282/2100] Allow notifications while the game is paused (or in break time) RFC. This is to allow notifications to show at the pause screen (specifically for #23967, where exports are now happening). Not sure about the break time part of this, but might be fine? The toasts are immediately flushed before break time ends. --- osu.Game/Overlays/NotificationOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 15e6c94b34..beebc9daaf 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -118,7 +118,7 @@ namespace osu.Game.Overlays private void updateProcessingMode() { - bool enabled = OverlayActivationMode.Value == OverlayActivation.All || State.Value == Visibility.Visible; + bool enabled = OverlayActivationMode.Value != OverlayActivation.Disabled || State.Value == Visibility.Visible; notificationsEnabler?.Cancel(); From ff8350bac6977f701dca04c31a8f5888137609a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jun 2023 17:43:52 +0900 Subject: [PATCH 0283/2100] Update framework --- 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 522d28dca7..66f518f3d5 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -11,7 +11,7 @@ manifestmerger.jar - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index d94c4a2df9..9cb20ee364 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 96396ca4ad..256d1e43c4 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -16,6 +16,6 @@ iossimulator-x64 - + From dc1b4a39aa1fd160a1877ce42da87c5604a0d8e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jun 2023 18:23:59 +0900 Subject: [PATCH 0284/2100] Fix presenting beatmaps while in a multiplayer room not working --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index a36c7e801e..c02237bdde 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -49,6 +49,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } + [Resolved] + private OsuGame game { get; set; } + private AddItemButton addItemButton; public MultiplayerMatchSubScreen(Room room) @@ -403,18 +406,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (!this.IsCurrentScreen()) return; - if (client.Room == null) - return; + // If there's only one playlist item and we are the host, assume we want to change it. Else we're add a new one. + PlaylistItem itemToEdit = client.IsHost && Room.Playlist.Count == 1 ? Room.Playlist.Single() : null; - if (!client.IsHost) - { - // todo: should handle this when the request queue is implemented. - // if we decide that the presentation should exit the user from the multiplayer game, the PresentBeatmap - // flow may need to change to support an "unable to present" return value. - return; - } + OpenSongSelection(itemToEdit); - this.Push(new MultiplayerMatchSongSelect(Room, Room.Playlist.Single(item => item.ID == client.Room.Settings.PlaylistItemId))); + // Re-run PresentBeatmap now that we've pushed a song select that can handle it. + game?.PresentBeatmap(beatmap.BeatmapSetInfo, b => b.ID == beatmap.BeatmapInfo.ID); } protected override void Dispose(bool isDisposing) From 10ed3787a00ac6d277e9032a2566da060a801e15 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jun 2023 18:27:19 +0900 Subject: [PATCH 0285/2100] Don't show song select screen when local user doesn't have permission to add an item --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index c02237bdde..5fc7099544 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -337,11 +337,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer updateCurrentItem(); - addItemButton.Alpha = client.IsHost || Room.QueueMode.Value != QueueMode.HostOnly ? 1 : 0; + addItemButton.Alpha = localUserCanAddItem ? 1 : 0; Scheduler.AddOnce(UpdateMods); } + private bool localUserCanAddItem => client.IsHost || Room.QueueMode.Value != QueueMode.HostOnly; + private void updateCurrentItem() { Debug.Assert(client.Room != null); @@ -406,6 +408,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (!this.IsCurrentScreen()) return; + if (!localUserCanAddItem) + return; + // If there's only one playlist item and we are the host, assume we want to change it. Else we're add a new one. PlaylistItem itemToEdit = client.IsHost && Room.Playlist.Count == 1 ? Room.Playlist.Single() : null; From 2e02b4a85b628588bdc8af93d92095ddfabbd3fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jun 2023 18:35:51 +0900 Subject: [PATCH 0286/2100] Apply more correct fix for double-playing menu track --- osu.Game/Overlays/MusicController.cs | 10 ++++------ osu.Game/Screens/Select/SongSelect.cs | 3 --- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 1ad5a8c08b..0d175a624c 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -316,6 +316,8 @@ namespace osu.Game.Overlays var queuedTrack = getQueuedTrack(); var lastTrack = CurrentTrack; + lastTrack.Completed -= onTrackCompleted; + CurrentTrack = queuedTrack; // At this point we may potentially be in an async context from tests. This is extremely dangerous but we have to make do for now. @@ -344,16 +346,12 @@ namespace osu.Game.Overlays // Important to keep this in its own method to avoid inadvertently capturing unnecessary variables in the callback. // Can lead to leaks. var queuedTrack = new DrawableTrack(current.LoadTrack()); - queuedTrack.Completed += () => onTrackCompleted(current); + queuedTrack.Completed += onTrackCompleted; return queuedTrack; } - private void onTrackCompleted(WorkingBeatmap workingBeatmap) + private void onTrackCompleted() { - // the source of track completion is the audio thread, so the beatmap may have changed before firing. - if (current != workingBeatmap) - return; - if (!CurrentTrack.Looping && !beatmap.Disabled) NextTrack(); } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index c232b7f490..47e5325baf 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -813,9 +813,6 @@ namespace osu.Game.Screens.Select if (!ControlGlobalMusic) return; - if (Beatmap.Value is DummyWorkingBeatmap) - return; - ITrack track = music.CurrentTrack; bool isNewTrack = !lastTrack.TryGetTarget(out var last) || last != track; From d7b486e2ac306a82e59a8958623c59b6aaed1384 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jun 2023 19:18:17 +0900 Subject: [PATCH 0287/2100] Disable beatmap skinning when entering the skin editor --- .../Overlays/SkinEditor/SkinEditorOverlay.cs | 58 ++++++++++++++++--- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 1c0ece28fe..b120faa45f 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -3,16 +3,19 @@ using System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using osu.Game.Screens; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; +using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Overlays.SkinEditor @@ -45,6 +48,12 @@ namespace osu.Game.Overlays.SkinEditor RelativeSizeAxes = Axes.Both; } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins); + } + public bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) @@ -147,17 +156,24 @@ namespace osu.Game.Overlays.SkinEditor /// public void SetTarget(OsuScreen screen) { - lastTargetScreen = screen; + try + { + lastTargetScreen = screen; - if (skinEditor == null) return; + if (skinEditor == null) return; - skinEditor.Save(userTriggered: false); + skinEditor.Save(userTriggered: false); - // ensure the toolbar is re-hidden even if a new screen decides to try and show it. - updateComponentVisibility(); + // ensure the toolbar is re-hidden even if a new screen decides to try and show it. + updateComponentVisibility(); - // AddOnce with parameter will ensure the newest target is loaded if there is any overlap. - Scheduler.AddOnce(setTarget, screen); + // AddOnce with parameter will ensure the newest target is loaded if there is any overlap. + Scheduler.AddOnce(setTarget, screen); + } + finally + { + globallyReenableBeatmapSkinSetting(); + } } private void setTarget(OsuScreen? target) @@ -173,6 +189,9 @@ namespace osu.Game.Overlays.SkinEditor return; } + if (target is Player) + globallyDisableBeatmapSkinSetting(); + if (skinEditor.State.Value == Visibility.Visible) skinEditor.UpdateTargetScreen(target); else @@ -182,5 +201,30 @@ namespace osu.Game.Overlays.SkinEditor skinEditor = null; } } + + private readonly Bindable beatmapSkins = new Bindable(); + private bool beatmapSkinsOriginalState; + + private void globallyDisableBeatmapSkinSetting() + { + if (beatmapSkins.Disabled) + return; + + // The skin editor doesn't work well if beatmap skins are being applied to the player screen. + // To keep things simple, disable the setting game-wide while using the skin editor. + beatmapSkinsOriginalState = beatmapSkins.Value; + + beatmapSkins.Value = false; + beatmapSkins.Disabled = true; + } + + private void globallyReenableBeatmapSkinSetting() + { + if (!beatmapSkins.Disabled) + return; + + beatmapSkins.Disabled = false; + beatmapSkins.Value = beatmapSkinsOriginalState; + } } } From 555ce7684b9d57183a603f7469dc23bd36354d83 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jun 2023 20:04:02 +0900 Subject: [PATCH 0288/2100] Adjust `GameplaySampleTriggerSource` to only switch samples when close enough to the next hit object Closes #23963. To simplify things, I've removed the optimisation of using `AliveObject`s because it would break the way this whole lookup works. --- .../UI/GameplaySampleTriggerSource.cs | 56 +++++++------------ 1 file changed, 19 insertions(+), 37 deletions(-) diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index fbb7a20a5d..909b633a72 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -7,7 +7,7 @@ using System.Linq; using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; namespace osu.Game.Rulesets.UI @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.UI }; } - private HitObjectLifetimeEntry fallbackObject; + private HitObjectLifetimeEntry mostValidObject; /// /// Play the most appropriate hit sound for the current point in time. @@ -67,56 +67,38 @@ namespace osu.Game.Rulesets.UI protected HitObject GetMostValidObject() { - // The most optimal lookup case we have is when an object is alive. There are usually very few alive objects so there's no drawbacks in attempting this lookup each time. - var drawableHitObject = hitObjectContainer.AliveObjects.FirstOrDefault(h => h.Result?.HasResult != true); - - if (drawableHitObject != null) - { - // A hit object may have a more valid nested object. - drawableHitObject = getMostValidNestedDrawable(drawableHitObject); - - return drawableHitObject.HitObject; - } - - // In the case a next object isn't available in drawable form, we need to do a somewhat expensive traversal to get a valid sound to play. - // This lookup can be skipped if the last entry is still valid (in the future and not yet hit). - if (fallbackObject == null || fallbackObject.Result?.HasResult == true) + if (mostValidObject == null || isAlreadyHit(mostValidObject)) { // We need to use lifetime entries to find the next object (we can't just use `hitObjectContainer.Objects` due to pooling - it may even be empty). // If required, we can make this lookup more efficient by adding support to get next-future-entry in LifetimeEntryManager. - fallbackObject = hitObjectContainer.Entries - .Where(e => e.Result?.HasResult != true).MinBy(e => e.HitObject.StartTime); - - if (fallbackObject != null) - return getEarliestNestedObject(fallbackObject.HitObject); + var candidate = hitObjectContainer.Entries.Where(e => !isAlreadyHit(e)).MinBy(e => e.HitObject.StartTime); // In the case there are no non-judged objects, the last hit object should be used instead. - fallbackObject ??= hitObjectContainer.Entries.LastOrDefault(); + if (candidate == null) + mostValidObject = hitObjectContainer.Entries.LastOrDefault(); + else + { + if (isCloseEnoughToCurrentTime(candidate)) + mostValidObject = candidate; + else + mostValidObject ??= hitObjectContainer.Entries.FirstOrDefault(); + } } - if (fallbackObject == null) + if (mostValidObject == null) return null; - bool fallbackHasResult = fallbackObject.Result?.HasResult == true; - // If the fallback has been judged then we want the sample from the object itself. - if (fallbackHasResult) - return fallbackObject.HitObject; + if (isAlreadyHit(mostValidObject)) + return mostValidObject.HitObject; // Else we want the earliest (including nested). // In cases of nested objects, they will always have earlier sample data than their parent object. - return getEarliestNestedObject(fallbackObject.HitObject); + return getEarliestNestedObject(mostValidObject.HitObject); } - private DrawableHitObject getMostValidNestedDrawable(DrawableHitObject o) - { - var nestedWithoutResult = o.NestedHitObjects.FirstOrDefault(n => n.Result?.HasResult != true); - - if (nestedWithoutResult == null) - return o; - - return getMostValidNestedDrawable(nestedWithoutResult); - } + private bool isAlreadyHit(HitObjectLifetimeEntry h) => h.Result?.HasResult == true; + private bool isCloseEnoughToCurrentTime(HitObjectLifetimeEntry h) => Time.Current > h.HitObject.StartTime - h.HitObject.HitWindows.WindowFor(HitResult.Miss) * 1.5; private HitObject getEarliestNestedObject(HitObject hitObject) { From 786d5a394b526db81e5e5435edc486d777a9787a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jun 2023 20:30:07 +0900 Subject: [PATCH 0289/2100] Add back optimisation and increase time allowance slightly --- osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index 909b633a72..bc410acd9a 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -71,17 +71,26 @@ namespace osu.Game.Rulesets.UI { // We need to use lifetime entries to find the next object (we can't just use `hitObjectContainer.Objects` due to pooling - it may even be empty). // If required, we can make this lookup more efficient by adding support to get next-future-entry in LifetimeEntryManager. - var candidate = hitObjectContainer.Entries.Where(e => !isAlreadyHit(e)).MinBy(e => e.HitObject.StartTime); + var candidate = + // Use alive entries first as an optimisation. + hitObjectContainer.AliveEntries.Select(tuple => tuple.Entry).Where(e => !isAlreadyHit(e)).MinBy(e => e.HitObject.StartTime) + ?? hitObjectContainer.Entries.Where(e => !isAlreadyHit(e)).MinBy(e => e.HitObject.StartTime); // In the case there are no non-judged objects, the last hit object should be used instead. if (candidate == null) + { mostValidObject = hitObjectContainer.Entries.LastOrDefault(); + } else { if (isCloseEnoughToCurrentTime(candidate)) + { mostValidObject = candidate; + } else + { mostValidObject ??= hitObjectContainer.Entries.FirstOrDefault(); + } } } @@ -98,7 +107,7 @@ namespace osu.Game.Rulesets.UI } private bool isAlreadyHit(HitObjectLifetimeEntry h) => h.Result?.HasResult == true; - private bool isCloseEnoughToCurrentTime(HitObjectLifetimeEntry h) => Time.Current > h.HitObject.StartTime - h.HitObject.HitWindows.WindowFor(HitResult.Miss) * 1.5; + private bool isCloseEnoughToCurrentTime(HitObjectLifetimeEntry h) => Time.Current >= h.HitObject.StartTime - h.HitObject.HitWindows.WindowFor(HitResult.Miss) * 2; private HitObject getEarliestNestedObject(HitObject hitObject) { From 0e861026814443b4fff965674e2805b77e2f63d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jun 2023 20:45:02 +0900 Subject: [PATCH 0290/2100] Fix nested lookups --- .../Rulesets/UI/GameplaySampleTriggerSource.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index bc410acd9a..8de686a0d6 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics.Containers; using osu.Game.Audio; @@ -101,19 +102,23 @@ namespace osu.Game.Rulesets.UI if (isAlreadyHit(mostValidObject)) return mostValidObject.HitObject; - // Else we want the earliest (including nested). + // Else we want the earliest valid nested. // In cases of nested objects, they will always have earlier sample data than their parent object. - return getEarliestNestedObject(mostValidObject.HitObject); + return getAllNested(mostValidObject.HitObject).OrderBy(h => h.StartTime).FirstOrDefault(h => h.StartTime > Time.Current) ?? mostValidObject.HitObject; } private bool isAlreadyHit(HitObjectLifetimeEntry h) => h.Result?.HasResult == true; private bool isCloseEnoughToCurrentTime(HitObjectLifetimeEntry h) => Time.Current >= h.HitObject.StartTime - h.HitObject.HitWindows.WindowFor(HitResult.Miss) * 2; - private HitObject getEarliestNestedObject(HitObject hitObject) + private IEnumerable getAllNested(HitObject hitObject) { - var nested = hitObject.NestedHitObjects.FirstOrDefault(); + foreach (var h in hitObject.NestedHitObjects) + { + yield return h; - return nested != null ? getEarliestNestedObject(nested) : hitObject; + foreach (var n in getAllNested(h)) + yield return n; + } } private SkinnableSound getNextSample() From 04dad6c6e8a60dad3e7bcceeb5a13e75c0ac0d61 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jun 2023 20:47:56 +0900 Subject: [PATCH 0291/2100] Use `IGameplayClock` to ensure our clock source is correct --- .../Gameplay/TestSceneGameplaySampleTriggerSource.cs | 2 +- osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index e52ec6f8cc..ca4a608114 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Gameplay { base.SetUpSteps(); - AddStep("Add trigger source", () => Player.HUDOverlay.Add(sampleTriggerSource = new TestGameplaySampleTriggerSource(Player.DrawableRuleset.Playfield.HitObjectContainer))); + AddStep("Add trigger source", () => Player.GameplayClockContainer.Add(sampleTriggerSource = new TestGameplaySampleTriggerSource(Player.DrawableRuleset.Playfield.HitObjectContainer))); } [Test] diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index 8de686a0d6..029cab65ab 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -5,10 +5,12 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play; using osu.Game.Skinning; namespace osu.Game.Rulesets.UI @@ -29,6 +31,9 @@ namespace osu.Game.Rulesets.UI private readonly Container hitSounds; + [Resolved] + private IGameplayClock gameplayClock { get; set; } + public GameplaySampleTriggerSource(HitObjectContainer hitObjectContainer) { this.hitObjectContainer = hitObjectContainer; @@ -104,11 +109,11 @@ namespace osu.Game.Rulesets.UI // Else we want the earliest valid nested. // In cases of nested objects, they will always have earlier sample data than their parent object. - return getAllNested(mostValidObject.HitObject).OrderBy(h => h.StartTime).FirstOrDefault(h => h.StartTime > Time.Current) ?? mostValidObject.HitObject; + return getAllNested(mostValidObject.HitObject).OrderBy(h => h.StartTime).FirstOrDefault(h => h.StartTime > gameplayClock.CurrentTime) ?? mostValidObject.HitObject; } private bool isAlreadyHit(HitObjectLifetimeEntry h) => h.Result?.HasResult == true; - private bool isCloseEnoughToCurrentTime(HitObjectLifetimeEntry h) => Time.Current >= h.HitObject.StartTime - h.HitObject.HitWindows.WindowFor(HitResult.Miss) * 2; + private bool isCloseEnoughToCurrentTime(HitObjectLifetimeEntry h) => gameplayClock.CurrentTime >= h.HitObject.StartTime - h.HitObject.HitWindows.WindowFor(HitResult.Miss) * 2; private IEnumerable getAllNested(HitObject hitObject) { From 92e89c7df7f3f5894bb0e22de59dec1d5ea1d2ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jun 2023 21:02:34 +0900 Subject: [PATCH 0292/2100] Update test expectations --- .../TestSceneGameplaySampleTriggerSource.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index ca4a608114..ca76337568 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Storyboards; using osuTK; @@ -62,25 +63,30 @@ namespace osu.Game.Tests.Visual.Gameplay { new HitCircle { + HitWindows = new HitWindows(), StartTime = t += spacing, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } }, new HitCircle { + HitWindows = new HitWindows(), StartTime = t += spacing, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) } }, new HitCircle { + HitWindows = new HitWindows(), StartTime = t += spacing, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT) }, }, new HitCircle { + HitWindows = new HitWindows(), StartTime = t += spacing, }, new Slider { + HitWindows = new HitWindows(), StartTime = t += spacing, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }), Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT) }, @@ -131,7 +137,12 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("first object hit", () => getNextAliveObject()?.Entry?.Result?.HasResult == true); - checkValidObjectIndex(1); + // next object is too far away, so we still use the already hit object. + checkValidObjectIndex(0); + + // still too far away. + seekBeforeIndex(1, 400); + checkValidObjectIndex(0); // Still object 1 as it's not hit yet. seekBeforeIndex(1); @@ -168,9 +179,9 @@ namespace osu.Game.Tests.Visual.Gameplay checkValidObjectIndex(4); } - private void seekBeforeIndex(int index) + private void seekBeforeIndex(int index, double amount = 100) { - AddStep($"seek to just before object {index}", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects[index].StartTime - 100)); + AddStep($"seek to {amount} ms before object {index}", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects[index].StartTime - amount)); waitForCatchUp(); } From cb07f2399fc584069578e2791fb8ecf7f5fd5674 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jun 2023 21:03:55 +0900 Subject: [PATCH 0293/2100] Apply NRT to `GameplaySampleTriggerSource` --- osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index 029cab65ab..472f91ac27 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -31,8 +29,10 @@ namespace osu.Game.Rulesets.UI private readonly Container hitSounds; + private HitObjectLifetimeEntry? mostValidObject; + [Resolved] - private IGameplayClock gameplayClock { get; set; } + private IGameplayClock gameplayClock { get; set; } = null!; public GameplaySampleTriggerSource(HitObjectContainer hitObjectContainer) { @@ -45,14 +45,12 @@ namespace osu.Game.Rulesets.UI }; } - private HitObjectLifetimeEntry mostValidObject; - /// /// Play the most appropriate hit sound for the current point in time. /// public virtual void Play() { - var nextObject = GetMostValidObject(); + HitObject? nextObject = GetMostValidObject(); if (nextObject == null) return; @@ -71,7 +69,7 @@ namespace osu.Game.Rulesets.UI hitSound.Play(); }); - protected HitObject GetMostValidObject() + protected HitObject? GetMostValidObject() { if (mostValidObject == null || isAlreadyHit(mostValidObject)) { From 2f77675fe7f133bb60c26f9e8826f0ea534e2887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Jun 2023 21:57:32 +0200 Subject: [PATCH 0294/2100] Fix errors in tests due to mismatching NRT annotations --- .../TestSceneDrumSampleTriggerSource.cs | 2 +- .../Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs index 287d90b406..23e85d1ae0 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs @@ -331,7 +331,7 @@ namespace osu.Game.Rulesets.Taiko.Tests LastPlayedSamples = samples; } - public new HitObject GetMostValidObject() => base.GetMostValidObject(); + public new HitObject? GetMostValidObject() => base.GetMostValidObject(); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index ca76337568..6701871e8d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.Gameplay { } - public new HitObject GetMostValidObject() => base.GetMostValidObject(); + public new HitObject? GetMostValidObject() => base.GetMostValidObject(); } } } From 29697d4999ab144c08a37bc26443f916fd66fd1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Jun 2023 22:00:02 +0200 Subject: [PATCH 0295/2100] Fix taiko test scene failing due to missing gameplay clock dependency `GameplayClock` is inscrutable. `TestManualClock` is lifted from another test scene because of `FramedBeatmapClock`'s intensely confusing tendency to not work if it is given a non-adjustable `ManuelClock` instead. --- .../TestSceneDrumSampleTriggerSource.cs | 141 ++++++++++++++---- 1 file changed, 112 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs index 23e85d1ae0..f45b4e23e4 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs @@ -6,7 +6,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Timing; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -17,14 +16,13 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Play; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests { public partial class TestSceneDrumSampleTriggerSource : OsuTestScene { - private readonly ManualClock manualClock = new ManualClock(); - [Cached(typeof(IScrollingInfo))] private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo { @@ -34,23 +32,25 @@ namespace osu.Game.Rulesets.Taiko.Tests private ScrollingHitObjectContainer hitObjectContainer = null!; private TestDrumSampleTriggerSource triggerSource = null!; + private readonly ManualClock manualClock = new TestManualClock(); + private GameplayClockContainer gameplayClock = null!; [SetUp] public void SetUp() => Schedule(() => { - hitObjectContainer = new ScrollingHitObjectContainer(); - manualClock.CurrentTime = 0; - - Child = new Container + gameplayClock = new GameplayClockContainer(manualClock) { - Clock = new FramedClock(manualClock), RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - hitObjectContainer, + hitObjectContainer = new ScrollingHitObjectContainer(), triggerSource = new TestDrumSampleTriggerSource(hitObjectContainer) } }; + gameplayClock.Reset(0); + + hitObjectContainer.Clock = gameplayClock; + Child = gameplayClock; }); [Test] @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Tests checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); - AddStep("seek past hit", () => manualClock.CurrentTime = 200); + AddStep("seek past hit", () => gameplayClock.Seek(200)); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); @@ -103,12 +103,67 @@ namespace osu.Game.Rulesets.Taiko.Tests checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); - AddStep("seek past hit", () => manualClock.CurrentTime = 200); + seekTo(200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); } + [Test] + public void TestBetweenHits() + { + Hit first = null!, second = null!; + + AddStep("add hit with normal samples", () => + { + first = new Hit + { + StartTime = 100, + Samples = new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL) + } + }; + first.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + var drawableHit = new DrawableHit(first); + hitObjectContainer.Add(drawableHit); + }); + AddStep("add hit with soft samples", () => + { + second = new Hit + { + StartTime = 500, + Samples = new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT), + new HitSampleInfo(HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT) + } + }; + second.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + var drawableHit = new DrawableHit(second); + hitObjectContainer.Add(drawableHit); + }); + + AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first)); + checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); + checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); + + seekTo(120); + AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first)); + checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); + checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); + + seekTo(480); + AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second)); + checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + + seekTo(700); + AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second)); + checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + } + [Test] public void TestDrumStrongHit() { @@ -128,11 +183,11 @@ namespace osu.Game.Rulesets.Taiko.Tests hitObjectContainer.Add(drawableHit); }); - AddAssert("most valid object is strong nested hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); + AddAssert("most valid object is nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); - AddStep("seek past hit", () => manualClock.CurrentTime = 200); + seekTo(200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); @@ -161,12 +216,12 @@ namespace osu.Game.Rulesets.Taiko.Tests checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); - AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600); + seekTo(600); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); - AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200); + seekTo(1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); @@ -195,12 +250,12 @@ namespace osu.Game.Rulesets.Taiko.Tests checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); - AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600); + seekTo(600); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); - AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200); + seekTo(1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); @@ -226,16 +281,16 @@ namespace osu.Game.Rulesets.Taiko.Tests hitObjectContainer.Add(drawableDrumRoll); }); - AddAssert("most valid object is drum roll tick's nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); + AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); - AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600); - AddAssert("most valid object is drum roll tick's nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); + seekTo(600); + AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); - AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200); + seekTo(1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); @@ -260,16 +315,16 @@ namespace osu.Game.Rulesets.Taiko.Tests hitObjectContainer.Add(drawableSwell); }); - AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); + AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); - AddStep("seek to middle of swell", () => manualClock.CurrentTime = 600); - AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); + seekTo(600); + AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); - AddStep("seek past swell", () => manualClock.CurrentTime = 1200); + seekTo(1200); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); @@ -294,16 +349,16 @@ namespace osu.Game.Rulesets.Taiko.Tests hitObjectContainer.Add(drawableSwell); }); - AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); + AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); - AddStep("seek to middle of swell", () => manualClock.CurrentTime = 600); - AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); + seekTo(600); + AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); - AddStep("seek past swell", () => manualClock.CurrentTime = 1200); + seekTo(1200); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); @@ -316,6 +371,8 @@ namespace osu.Game.Rulesets.Taiko.Tests AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType().Single().Bank, () => Is.EqualTo(expectedBank)); } + private void seekTo(double time) => AddStep($"seek to {time}", () => gameplayClock.Seek(time)); + private partial class TestDrumSampleTriggerSource : DrumSampleTriggerSource { public ISampleInfo[]? LastPlayedSamples { get; private set; } @@ -333,5 +390,31 @@ namespace osu.Game.Rulesets.Taiko.Tests public new HitObject? GetMostValidObject() => base.GetMostValidObject(); } + + private class TestManualClock : ManualClock, IAdjustableClock + { + public TestManualClock() + { + IsRunning = true; + } + + public void Start() => IsRunning = true; + + public void Stop() => IsRunning = false; + + public bool Seek(double position) + { + CurrentTime = position; + return true; + } + + public void Reset() + { + } + + public void ResetSpeedAdjustments() + { + } + } } } From a7172e74699265b34898083317c38632e94599b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Jun 2023 23:32:38 +0200 Subject: [PATCH 0296/2100] Fix one remaining seek --- .../TestSceneDrumSampleTriggerSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs index f45b4e23e4..e8da7f6937 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Tests checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); - AddStep("seek past hit", () => gameplayClock.Seek(200)); + seekTo(200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); From 8460873e61e5b74e60cf2f39b817862f57a0f322 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Wed, 21 Jun 2023 00:37:54 -0400 Subject: [PATCH 0297/2100] move commitButton.Text update to appropriate method --- osu.Game/Overlays/Comments/CommentEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index b363fe5881..4898d8b7c7 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -169,13 +169,13 @@ namespace osu.Game.Overlays.Comments { bool textBoxValid = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value); commitButton.Enabled.Value = textBoxValid || !API.IsLoggedIn; + commitButton.Text = GetCommitButtonText(API.IsLoggedIn); } private void updateStateForLoggedIn() { TextBox.PlaceholderText = GetPlaceholderText(API.IsLoggedIn); TextBox.ReadOnly = !API.IsLoggedIn; - commitButton.Text = GetCommitButtonText(API.IsLoggedIn); updateCommitButtonState(); } From cc764afe3e6d9c6fd9385ecf13de3314b6dd698c Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Wed, 21 Jun 2023 00:58:43 -0400 Subject: [PATCH 0298/2100] use two separate buttons for posting / login --- osu.Game/Overlays/Comments/CommentEditor.cs | 46 ++++++++++++--------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index 4898d8b7c7..8f7c8ced57 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -31,6 +31,7 @@ namespace osu.Game.Overlays.Comments protected readonly Bindable Current = new Bindable(string.Empty); private RoundedButton commitButton = null!; + private RoundedButton logInButton = null!; private LoadingSpinner loadingSpinner = null!; protected TextBox TextBox { get; private set; } = null!; @@ -122,9 +123,19 @@ namespace osu.Game.Overlays.Comments AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(5, 0), - Child = commitButton = new EditorButton + Children = new Drawable[] { - Action = () => commitOrLogIn(Current.Value) + commitButton = new EditorButton + { + Action = () => OnCommit(Current.Value), + Text = GetCommitButtonText(true) + }, + logInButton = new EditorButton + { + Width = 100, + Action = () => loginOverlay?.Show(), + Text = GetCommitButtonText(false) + } } }, loadingSpinner = new LoadingSpinner @@ -154,29 +165,24 @@ namespace osu.Game.Overlays.Comments protected abstract void OnCommit(string text); - private void commitOrLogIn(string text) - { - if (!API.IsLoggedIn) - { - loginOverlay?.Show(); - return; - } - - OnCommit(text); - } - - private void updateCommitButtonState() - { - bool textBoxValid = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value); - commitButton.Enabled.Value = textBoxValid || !API.IsLoggedIn; - commitButton.Text = GetCommitButtonText(API.IsLoggedIn); - } + private void updateCommitButtonState() => + commitButton.Enabled.Value = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value); private void updateStateForLoggedIn() { TextBox.PlaceholderText = GetPlaceholderText(API.IsLoggedIn); TextBox.ReadOnly = !API.IsLoggedIn; - updateCommitButtonState(); + + if (API.IsLoggedIn) + { + commitButton.Show(); + logInButton.Hide(); + } + else + { + commitButton.Hide(); + logInButton.Show(); + } } private partial class EditorTextBox : OsuTextBox From dd4f271158b26a739ace7a37d0134a54c2f999bd Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Wed, 21 Jun 2023 02:15:02 -0400 Subject: [PATCH 0299/2100] fix cancel test for new button layout --- osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs index 0bc6367d64..97eaa5b9ec 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; @@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep("click cancel button", () => { - InputManager.MoveMouseTo(cancellableCommentEditor.ButtonsContainer[1]); + InputManager.MoveMouseTo(cancellableCommentEditor.ButtonsContainer[2]); InputManager.Click(MouseButton.Left); }); From 6de7328fef4353feb90679bff3dff93c7d2a2d72 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Wed, 21 Jun 2023 02:17:51 -0400 Subject: [PATCH 0300/2100] add test for comment when logging in and out --- .../UserInterface/TestSceneCommentEditor.cs | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs index 97eaa5b9ec..1dafa2ab0a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; @@ -11,6 +11,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Overlays.Comments; using osuTK; @@ -25,6 +26,7 @@ namespace osu.Game.Tests.Visual.UserInterface private TestCommentEditor commentEditor = null!; private TestCancellableCommentEditor cancellableCommentEditor = null!; + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; [SetUp] public void SetUp() => Schedule(() => @@ -96,6 +98,37 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("button is not loading", () => !commentEditor.IsSpinnerShown); } + [Test] + public void TestLoggingInAndOut() + { + void addLoggedInAsserts() + { + AddAssert("commit button visible", () => commentEditor.ButtonsContainer[0].Alpha == 1); + AddAssert("login button hidden", () => commentEditor.ButtonsContainer[1].Alpha == 0); + AddAssert("text box editable", () => !commentEditor.TextBox.ReadOnly); + } + + void addLoggedOutAsserts() + { + AddAssert("commit button hidden", () => commentEditor.ButtonsContainer[0].Alpha == 0); + AddAssert("login button visible", () => commentEditor.ButtonsContainer[1].Alpha == 1); + AddAssert("text box readonly", () => commentEditor.TextBox.ReadOnly); + } + + // there's also the case of starting logged out, but more annoying to test. + + // starting logged in + addLoggedInAsserts(); + + // moving from logged in -> logged out + AddStep("log out", () => dummyAPI.Logout()); + addLoggedOutAsserts(); + + // moving from logged out -> logged in + AddStep("log back in", () => dummyAPI.Login("username", "password")); + addLoggedInAsserts(); + } + [Test] public void TestCancelAction() { @@ -112,6 +145,7 @@ namespace osu.Game.Tests.Visual.UserInterface { public new Bindable Current => base.Current; public new FillFlowContainer ButtonsContainer => base.ButtonsContainer; + public new TextBox TextBox => base.TextBox; public string CommittedText { get; private set; } = string.Empty; @@ -125,8 +159,12 @@ namespace osu.Game.Tests.Visual.UserInterface } protected override LocalisableString FooterText => @"Footer text. And it is pretty long. Cool."; - protected override LocalisableString GetCommitButtonText(bool isLoggedIn) => @"Commit"; - protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => @"This text box is empty"; + + protected override LocalisableString GetCommitButtonText(bool isLoggedIn) => + isLoggedIn ? @"Commit" : "You're logged out!"; + + protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => + isLoggedIn ? @"This text box is empty" : "Still empty, but now you can't type in it."; } private partial class TestCancellableCommentEditor : CancellableCommentEditor From 366dd96875f673f7da8c72ed27028143372715ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Jun 2023 16:09:54 +0900 Subject: [PATCH 0301/2100] Use bindable lease instead of reimplementing the same thing locally --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index b120faa45f..4f0028de64 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -203,7 +203,7 @@ namespace osu.Game.Overlays.SkinEditor } private readonly Bindable beatmapSkins = new Bindable(); - private bool beatmapSkinsOriginalState; + private LeasedBindable? leasedBeatmapSkins; private void globallyDisableBeatmapSkinSetting() { @@ -212,19 +212,14 @@ namespace osu.Game.Overlays.SkinEditor // The skin editor doesn't work well if beatmap skins are being applied to the player screen. // To keep things simple, disable the setting game-wide while using the skin editor. - beatmapSkinsOriginalState = beatmapSkins.Value; - - beatmapSkins.Value = false; - beatmapSkins.Disabled = true; + leasedBeatmapSkins = beatmapSkins.BeginLease(true); + leasedBeatmapSkins.Value = false; } private void globallyReenableBeatmapSkinSetting() { - if (!beatmapSkins.Disabled) - return; - - beatmapSkins.Disabled = false; - beatmapSkins.Value = beatmapSkinsOriginalState; + leasedBeatmapSkins?.Return(); + leasedBeatmapSkins = null; } } } From cb0f642ad786b6dba97b6cca042e9984000795dc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Jun 2023 16:11:19 +0900 Subject: [PATCH 0302/2100] Change skin editor flow to always save on toggle This also moves the beatmap skin disable toggle to on toggle, in line with review feedback. I've decided to always apply the disable, not just on the `Player` screen. It should be assumed that if a user is in the skin editor they are never going to need access to this anyway. --- .../Overlays/SkinEditor/SkinEditorOverlay.cs | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 4f0028de64..2dd30ca633 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -71,6 +71,8 @@ namespace osu.Game.Overlays.SkinEditor protected override void PopIn() { + globallyDisableBeatmapSkinSetting(); + if (skinEditor != null) { skinEditor.Show(); @@ -96,7 +98,13 @@ namespace osu.Game.Overlays.SkinEditor }); } - protected override void PopOut() => skinEditor?.Hide(); + protected override void PopOut() + { + skinEditor?.Save(false); + skinEditor?.Hide(); + + globallyReenableBeatmapSkinSetting(); + } protected override void Update() { @@ -156,24 +164,15 @@ namespace osu.Game.Overlays.SkinEditor /// public void SetTarget(OsuScreen screen) { - try - { - lastTargetScreen = screen; + lastTargetScreen = screen; - if (skinEditor == null) return; + if (skinEditor == null) return; - skinEditor.Save(userTriggered: false); + // ensure the toolbar is re-hidden even if a new screen decides to try and show it. + updateComponentVisibility(); - // ensure the toolbar is re-hidden even if a new screen decides to try and show it. - updateComponentVisibility(); - - // AddOnce with parameter will ensure the newest target is loaded if there is any overlap. - Scheduler.AddOnce(setTarget, screen); - } - finally - { - globallyReenableBeatmapSkinSetting(); - } + // AddOnce with parameter will ensure the newest target is loaded if there is any overlap. + Scheduler.AddOnce(setTarget, screen); } private void setTarget(OsuScreen? target) @@ -189,9 +188,6 @@ namespace osu.Game.Overlays.SkinEditor return; } - if (target is Player) - globallyDisableBeatmapSkinSetting(); - if (skinEditor.State.Value == Visibility.Visible) skinEditor.UpdateTargetScreen(target); else From 4ff52752082f93906df68ef0d8cb1a1048d44f92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Jun 2023 17:33:42 +0900 Subject: [PATCH 0303/2100] Make `IGameplayClock` optional in `GameplaySampleTriggerSource` to ease testing --- osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index 472f91ac27..b67e977822 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.UI private HitObjectLifetimeEntry? mostValidObject; [Resolved] - private IGameplayClock gameplayClock { get; set; } = null!; + private IGameplayClock? gameplayClock { get; set; } public GameplaySampleTriggerSource(HitObjectContainer hitObjectContainer) { @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.UI } else { - if (isCloseEnoughToCurrentTime(candidate)) + if (isCloseEnoughToCurrentTime(candidate.HitObject)) { mostValidObject = candidate; } @@ -107,11 +107,13 @@ namespace osu.Game.Rulesets.UI // Else we want the earliest valid nested. // In cases of nested objects, they will always have earlier sample data than their parent object. - return getAllNested(mostValidObject.HitObject).OrderBy(h => h.StartTime).FirstOrDefault(h => h.StartTime > gameplayClock.CurrentTime) ?? mostValidObject.HitObject; + return getAllNested(mostValidObject.HitObject).OrderBy(h => h.StartTime).FirstOrDefault(h => h.StartTime > getReferenceTime()) ?? mostValidObject.HitObject; } private bool isAlreadyHit(HitObjectLifetimeEntry h) => h.Result?.HasResult == true; - private bool isCloseEnoughToCurrentTime(HitObjectLifetimeEntry h) => gameplayClock.CurrentTime >= h.HitObject.StartTime - h.HitObject.HitWindows.WindowFor(HitResult.Miss) * 2; + private bool isCloseEnoughToCurrentTime(HitObject h) => getReferenceTime() >= h.StartTime - h.HitWindows.WindowFor(HitResult.Miss) * 2; + + private double getReferenceTime() => (gameplayClock?.CurrentTime ?? Clock.CurrentTime); private IEnumerable getAllNested(HitObject hitObject) { From 9ca772421d470c8a2a86448413fc15f2c38b2fd2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Jun 2023 17:48:12 +0900 Subject: [PATCH 0304/2100] Improve and combine logic that exists in two classes --- .../Screens/Play/SaveFailedScoreButton.cs | 25 +++++++++++++---- .../Screens/Ranking/ReplayDownloadButton.cs | 28 +++++++++++-------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Play/SaveFailedScoreButton.cs b/osu.Game/Screens/Play/SaveFailedScoreButton.cs index dc0ac054cb..dd3b2d676d 100644 --- a/osu.Game/Screens/Play/SaveFailedScoreButton.cs +++ b/osu.Game/Screens/Play/SaveFailedScoreButton.cs @@ -97,6 +97,8 @@ namespace osu.Game.Screens.Play }, true); } + #region Export via hotkey logic (also in ReplayDownloadButton) + public bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) @@ -106,12 +108,12 @@ namespace osu.Game.Screens.Play return true; case GlobalAction.ExportReplay: - Task.Run(importFailedScore).ContinueWith(t => - { - importedScore = realm.Run(r => r.Find(t.GetResultSafely().ID)?.Detach()); - Schedule(() => state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded); - scoreManager.Export(importedScore); - }); + state.BindValueChanged(exportWhenReady, true); + + // start the import via button + if (state.Value != DownloadState.LocallyAvailable) + button.TriggerClick(); + return true; } @@ -121,5 +123,16 @@ namespace osu.Game.Screens.Play public void OnReleased(KeyBindingReleaseEvent e) { } + + private void exportWhenReady(ValueChangedEvent state) + { + if (state.NewValue != DownloadState.LocallyAvailable) return; + + scoreManager.Export(importedScore); + + this.state.ValueChanged -= exportWhenReady; + } + + #endregion } } diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index 0772f54860..799041b7de 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -103,6 +103,8 @@ namespace osu.Game.Screens.Ranking }, true); } + #region Export via hotkey logic (also in SaveFailedScoreButton) + public bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) @@ -112,18 +114,11 @@ namespace osu.Game.Screens.Ranking return true; case GlobalAction.ExportReplay: - if (State.Value == DownloadState.NotDownloaded) - { + State.BindValueChanged(exportWhenReady, true); + + // start the import via button + if (State.Value != DownloadState.LocallyAvailable) button.TriggerClick(); - } - - State.ValueChanged += importAfterDownload; - - void importAfterDownload(ValueChangedEvent valueChangedEvent) - { - scoreManager.Export(Score.Value); - State.ValueChanged -= importAfterDownload; - } return true; } @@ -135,6 +130,17 @@ namespace osu.Game.Screens.Ranking { } + private void exportWhenReady(ValueChangedEvent state) + { + if (state.NewValue != DownloadState.LocallyAvailable) return; + + scoreManager.Export(Score.Value); + + State.ValueChanged -= exportWhenReady; + } + + #endregion + private void updateState() { switch (replayAvailability) From 1907beb0c9b4c48b32da711c7f762895813f5704 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Jun 2023 17:48:55 +0900 Subject: [PATCH 0305/2100] Add `FireAndForget` to stray `Task.Run` --- osu.Game/Screens/Play/SaveFailedScoreButton.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SaveFailedScoreButton.cs b/osu.Game/Screens/Play/SaveFailedScoreButton.cs index dd3b2d676d..0a2696339c 100644 --- a/osu.Game/Screens/Play/SaveFailedScoreButton.cs +++ b/osu.Game/Screens/Play/SaveFailedScoreButton.cs @@ -15,6 +15,7 @@ using osu.Game.Scoring; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Online; +using osu.Game.Online.Multiplayer; using osuTK; namespace osu.Game.Screens.Play @@ -63,7 +64,7 @@ namespace osu.Game.Screens.Play { importedScore = realm.Run(r => r.Find(t.GetResultSafely().ID)?.Detach()); Schedule(() => state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded); - }); + }).FireAndForget(); break; } } From 655491ae2d4532de479538d696229bb9095c82f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Jun 2023 17:50:06 +0900 Subject: [PATCH 0306/2100] Fix potential null ref in `ResultsScreen` --- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 78239e0dbe..b9f3b65129 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -160,7 +160,7 @@ namespace osu.Game.Screens.Ranking if (allowWatchingReplay) { - buttons.Add(new ReplayDownloadButton(null) + buttons.Add(new ReplayDownloadButton(SelectedScore.Value) { Score = { BindTarget = SelectedScore }, Width = 300 From c3f772f0da35dd16589c993835a5e1da1824d53d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Jun 2023 18:29:34 +0900 Subject: [PATCH 0307/2100] Add method to queue a restart after app is exited (when supported) --- osu.Desktop/OsuGameDesktop.cs | 20 +++++++++++++++++++ .../Screens/Setup/TournamentSwitcher.cs | 6 +++++- osu.Game/OsuGameBase.cs | 6 ++++++ .../Sections/Graphics/RendererSettings.cs | 13 +++++++++--- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 21cea3ba76..efd3d358b7 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -17,6 +17,7 @@ using osu.Game.Updater; using osu.Desktop.Windows; using osu.Game.IO; using osu.Game.IPC; +using osu.Game.Online.Multiplayer; using osu.Game.Utils; using SDL2; @@ -108,6 +109,25 @@ namespace osu.Desktop } } + public override bool RestartAppWhenExited() + { + switch (RuntimeInfo.OS) + { + case RuntimeInfo.Platform.Windows: + Debug.Assert(OperatingSystem.IsWindows()); + + // Of note, this is an async method in squirrel that adds an arbitrary delay before returning + // likely to ensure the external process is in a good state. + // + // We're not waiting on that here, but the outro playing before the actual exit should be enough + // to cover this. + Squirrel.UpdateManager.RestartAppWhenExited().FireAndForget(); + return true; + } + + return base.RestartAppWhenExited(); + } + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs index 7a8b03a7aa..01d86274d7 100644 --- a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs +++ b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs @@ -28,7 +28,11 @@ namespace osu.Game.Tournament.Screens.Setup dropdown.Items = storage.ListTournaments(); dropdown.Current.BindValueChanged(v => Button.Enabled.Value = v.NewValue != startupTournament, true); - Action = () => game.AttemptExit(); + Action = () => + { + game.RestartAppWhenExited(); + game.AttemptExit(); + }; folderButton.Action = () => storage.PresentExternally(); ButtonText = "Close osu!"; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 63efe0e2c8..6737caa5f9 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -515,6 +515,12 @@ namespace osu.Game Scheduler.AddDelayed(AttemptExit, 2000); } + /// + /// If supported by the platform, the game will automatically restart after the next exit. + /// + /// Whether a restart operation was queued. + public virtual bool RestartAppWhenExited() => false; + public bool Migrate(string path) { Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""..."); diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs index a1f728ca87..d4cef3f4d1 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs @@ -67,10 +67,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics if (r.NewValue == RendererType.Automatic && automaticRendererInUse) return; - dialogOverlay?.Push(new ConfirmDialog(GraphicsSettingsStrings.ChangeRendererConfirmation, () => game?.AttemptExit(), () => + if (game?.RestartAppWhenExited() == true) { - renderer.Value = automaticRendererInUse ? RendererType.Automatic : host.ResolvedRenderer; - })); + game.AttemptExit(); + } + else + { + dialogOverlay?.Push(new ConfirmDialog(GraphicsSettingsStrings.ChangeRendererConfirmation, () => game?.AttemptExit(), () => + { + renderer.Value = automaticRendererInUse ? RendererType.Automatic : host.ResolvedRenderer; + })); + } }); // TODO: remove this once we support SDL+android. From 59b1f08d530fa1fa254a102ac678d0f34830da6d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Jun 2023 18:27:29 +0900 Subject: [PATCH 0308/2100] Don't require exit confirmation when there are no ongoing operations that could be interrupted --- osu.Game/OsuGame.cs | 16 +++++++++++++--- osu.Game/Overlays/NotificationOverlay.cs | 2 ++ osu.Game/Screens/Menu/MainMenu.cs | 2 ++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a80639d4ff..7bfe88f248 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -83,7 +83,7 @@ namespace osu.Game /// protected const float SIDE_OVERLAY_OFFSET_RATIO = 0.05f; - public Toolbar Toolbar; + public Toolbar Toolbar { get; private set; } private ChatOverlay chatOverlay; @@ -778,8 +778,18 @@ namespace osu.Game public override void AttemptExit() { - // Using PerformFromScreen gives the user a chance to interrupt the exit process if needed. - PerformFromScreen(menu => menu.Exit()); + bool requiresConfirmationToExit = Notifications.HasOngoingOperations; + + PerformFromScreen(menu => + { + var mainMenu = ((MainMenu)menu); + + // The main menu exit implementation gives the user a chance to interrupt the exit process if needed. + if (requiresConfirmationToExit) + mainMenu.Exit(); + else + mainMenu.ExitWithoutConfirmation(); + }, new[] { typeof(MainMenu) }); } /// diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 15e6c94b34..c53b6b667c 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -33,6 +33,8 @@ namespace osu.Game.Overlays public const float TRANSITION_LENGTH = 600; + public bool HasOngoingOperations => sections.Any(s => s.Children.OfType().Any()); + private FlowContainer sections = null!; [Resolved] diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 69b8596474..998e36c2be 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -154,6 +154,8 @@ namespace osu.Game.Screens.Menu public void ReturnToOsuLogo() => Buttons.State = ButtonSystemState.Initial; + public void ExitWithoutConfirmation() => confirmAndExit(); + private void confirmAndExit() { if (exitConfirmed) return; From 7b4cbea362e0d40fd7ca180f6f1146af655dcdb0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Jun 2023 00:01:48 +0900 Subject: [PATCH 0309/2100] Allow nullable to fix test usages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../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 5fc7099544..1f9edfe97c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -49,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } - [Resolved] + [Resolved(canBeNull: true)] private OsuGame game { get; set; } private AddItemButton addItemButton; From 07a00e8afd1aa50cffda6d980f8209d8b72a5973 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Jun 2023 00:02:02 +0900 Subject: [PATCH 0310/2100] Fix typo in comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../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 1f9edfe97c..978d77b4f1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -411,7 +411,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (!localUserCanAddItem) return; - // If there's only one playlist item and we are the host, assume we want to change it. Else we're add a new one. + // If there's only one playlist item and we are the host, assume we want to change it. Else add a new one. PlaylistItem itemToEdit = client.IsHost && Room.Playlist.Count == 1 ? Room.Playlist.Single() : null; OpenSongSelection(itemToEdit); From 4be8eede8834dc473f19629b2ba83c79d1f9003e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Jun 2023 18:46:42 +0900 Subject: [PATCH 0311/2100] Fix combo counter on legacy skins flipping when "Floating Fruits" mod is active Closes #23989. --- .../Skinning/Legacy/LegacyCatchComboCounter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs index eba837a52d..55b24b3ffa 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Catch.UI; using osu.Game.Skinning; using osuTK; @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy /// /// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter. /// - public partial class LegacyCatchComboCounter : CompositeDrawable, ICatchComboCounter + public partial class LegacyCatchComboCounter : UprightAspectMaintainingContainer, ICatchComboCounter { private readonly LegacyRollingCounter counter; From 79606317ab671c93bc6c8d650f25240d1ed52cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 22 Jun 2023 20:02:10 +0200 Subject: [PATCH 0312/2100] Remove redundant parentheses --- osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index b67e977822..d8f421e54f 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.UI private bool isAlreadyHit(HitObjectLifetimeEntry h) => h.Result?.HasResult == true; private bool isCloseEnoughToCurrentTime(HitObject h) => getReferenceTime() >= h.StartTime - h.HitWindows.WindowFor(HitResult.Miss) * 2; - private double getReferenceTime() => (gameplayClock?.CurrentTime ?? Clock.CurrentTime); + private double getReferenceTime() => gameplayClock?.CurrentTime ?? Clock.CurrentTime; private IEnumerable getAllNested(HitObject hitObject) { From aea5eb37dca6e2ca7e5ad18e141d48fb49bc3b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 22 Jun 2023 20:24:44 +0200 Subject: [PATCH 0313/2100] Remove unused using directive --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 2dd30ca633..68d6b7ced5 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -15,7 +15,6 @@ using osu.Game.Input.Bindings; using osu.Game.Screens; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; -using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Overlays.SkinEditor From 21bed336c681abf31577834ca2fb830ea6a61497 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 22 Jun 2023 16:01:12 -0400 Subject: [PATCH 0314/2100] adjust DummyAPIAccess to more closely match APIAccess wrt logging in and out --- osu.Game/Online/API/DummyAPIAccess.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 16afef8e30..896fa30ff8 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -34,7 +34,7 @@ namespace osu.Game.Online.API public string AccessToken => "token"; - public bool IsLoggedIn => State.Value == APIState.Online; + public bool IsLoggedIn => State.Value > APIState.Offline; public string ProvidedUsername => LocalUser.Value.Username; @@ -114,8 +114,8 @@ namespace osu.Game.Online.API public void Logout() { - LocalUser.Value = new GuestUser(); state.Value = APIState.Offline; + LocalUser.Value = new GuestUser(); } public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null; From 786deec2964134b6a9ea5af897c0345e2159b090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 22 Jun 2023 23:00:52 +0200 Subject: [PATCH 0315/2100] Rename and xmldoc members --- .../UserInterface/TestSceneCommentEditor.cs | 4 ++-- osu.Game/Overlays/Comments/CommentEditor.cs | 15 ++++++++++++--- osu.Game/Overlays/Comments/CommentsContainer.cs | 2 +- osu.Game/Overlays/Comments/ReplyCommentEditor.cs | 2 +- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs index 1dafa2ab0a..1c3c86442e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs @@ -160,7 +160,7 @@ namespace osu.Game.Tests.Visual.UserInterface protected override LocalisableString FooterText => @"Footer text. And it is pretty long. Cool."; - protected override LocalisableString GetCommitButtonText(bool isLoggedIn) => + protected override LocalisableString GetButtonText(bool isLoggedIn) => isLoggedIn ? @"Commit" : "You're logged out!"; protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => @@ -184,7 +184,7 @@ namespace osu.Game.Tests.Visual.UserInterface { } - protected override LocalisableString GetCommitButtonText(bool isLoggedIn) => @"Save"; + protected override LocalisableString GetButtonText(bool isLoggedIn) => @"Save"; protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => @"Multiline textboxes soon"; } } diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index 8f7c8ced57..8dbafc0ea8 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -44,8 +44,17 @@ namespace osu.Game.Overlays.Comments [Resolved] private LoginOverlay? loginOverlay { get; set; } - protected abstract LocalisableString GetCommitButtonText(bool isLoggedIn); + /// + /// Returns the text content of the main action button. + /// When is , the text will apply to a button that posts a comment. + /// When is , the text will apply to a button that directs the user to the login overlay. + /// + protected abstract LocalisableString GetButtonText(bool isLoggedIn); + /// + /// Returns the placeholder text for the comment box. + /// + /// Whether the current user is logged in. protected abstract LocalisableString GetPlaceholderText(bool isLoggedIn); protected bool ShowLoadingSpinner @@ -128,13 +137,13 @@ namespace osu.Game.Overlays.Comments commitButton = new EditorButton { Action = () => OnCommit(Current.Value), - Text = GetCommitButtonText(true) + Text = GetButtonText(true) }, logInButton = new EditorButton { Width = 100, Action = () => loginOverlay?.Show(), - Text = GetCommitButtonText(false) + Text = GetButtonText(false) } } }, diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index e6c69d2090..af5f4dd280 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -410,7 +410,7 @@ namespace osu.Game.Overlays.Comments //TODO should match web, left empty due to no multiline support protected override LocalisableString FooterText => default; - protected override LocalisableString GetCommitButtonText(bool isLoggedIn) => + protected override LocalisableString GetButtonText(bool isLoggedIn) => isLoggedIn ? CommonStrings.ButtonsPost : CommentsStrings.GuestButtonNew; protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => diff --git a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs index 0d210021b9..dd4c35ef20 100644 --- a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs +++ b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs @@ -23,7 +23,7 @@ namespace osu.Game.Overlays.Comments protected override LocalisableString FooterText => default; - protected override LocalisableString GetCommitButtonText(bool isLoggedIn) => + protected override LocalisableString GetButtonText(bool isLoggedIn) => isLoggedIn ? CommonStrings.ButtonsReply : CommentsStrings.GuestButtonReply; protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => From 1672608a87311bde31d35616389d5db9603aa17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 22 Jun 2023 23:08:30 +0200 Subject: [PATCH 0316/2100] Document why things were done in `DummyAPIAccess` --- osu.Game/Online/API/DummyAPIAccess.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 896fa30ff8..bf9baa4414 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -34,6 +34,7 @@ namespace osu.Game.Online.API public string AccessToken => "token"; + /// public bool IsLoggedIn => State.Value > APIState.Offline; public string ProvidedUsername => LocalUser.Value.Username; @@ -115,6 +116,8 @@ namespace osu.Game.Online.API public void Logout() { state.Value = APIState.Offline; + // must happen after `state.Value` is changed such that subscribers to that bindable's value changes see the correct user. + // compare: `APIAccess.Logout()`. LocalUser.Value = new GuestUser(); } From 2c1c20e0a7b95b2b9805a1126d3ceb5157e3140f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 22 Jun 2023 23:10:16 +0200 Subject: [PATCH 0317/2100] Rename test helpers --- .../Visual/UserInterface/TestSceneCommentEditor.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs index 1c3c86442e..b17024ae8f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs @@ -101,14 +101,14 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestLoggingInAndOut() { - void addLoggedInAsserts() + void assertLoggedInState() { AddAssert("commit button visible", () => commentEditor.ButtonsContainer[0].Alpha == 1); AddAssert("login button hidden", () => commentEditor.ButtonsContainer[1].Alpha == 0); AddAssert("text box editable", () => !commentEditor.TextBox.ReadOnly); } - void addLoggedOutAsserts() + void assertLoggedOutState() { AddAssert("commit button hidden", () => commentEditor.ButtonsContainer[0].Alpha == 0); AddAssert("login button visible", () => commentEditor.ButtonsContainer[1].Alpha == 1); @@ -118,15 +118,15 @@ namespace osu.Game.Tests.Visual.UserInterface // there's also the case of starting logged out, but more annoying to test. // starting logged in - addLoggedInAsserts(); + assertLoggedInState(); // moving from logged in -> logged out AddStep("log out", () => dummyAPI.Logout()); - addLoggedOutAsserts(); + assertLoggedOutState(); // moving from logged out -> logged in AddStep("log back in", () => dummyAPI.Login("username", "password")); - addLoggedInAsserts(); + assertLoggedInState(); } [Test] From 64b726d5ecaaf1ef6c3b71bed4523990117300e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Jun 2023 13:48:13 +0900 Subject: [PATCH 0318/2100] Fix nested logic not being completely correct (favouring already-passed rather than near-future) --- osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index d8f421e54f..c554318357 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.UI // Else we want the earliest valid nested. // In cases of nested objects, they will always have earlier sample data than their parent object. - return getAllNested(mostValidObject.HitObject).OrderBy(h => h.StartTime).FirstOrDefault(h => h.StartTime > getReferenceTime()) ?? mostValidObject.HitObject; + return getAllNested(mostValidObject.HitObject).OrderBy(h => h.GetEndTime()).SkipWhile(h => h.GetEndTime() <= getReferenceTime()).FirstOrDefault() ?? mostValidObject.HitObject; } private bool isAlreadyHit(HitObjectLifetimeEntry h) => h.Result?.HasResult == true; From d53996336cb81a217e06f998ebdc440c17cf1145 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Jun 2023 14:01:10 +0900 Subject: [PATCH 0319/2100] Add note about swells and their ticks --- .../TestSceneDrumSampleTriggerSource.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs index e8da7f6937..bce855ae45 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs @@ -315,6 +315,9 @@ namespace osu.Game.Rulesets.Taiko.Tests hitObjectContainer.Add(drawableSwell); }); + // You might think that this should be a SwellTick since we're before the swell, but SwellTicks get no StartTime (ie. they are zero). + // This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits. + // But for sample playback purposes they can be ignored as noise. AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); @@ -349,6 +352,9 @@ namespace osu.Game.Rulesets.Taiko.Tests hitObjectContainer.Add(drawableSwell); }); + // You might think that this should be a SwellTick since we're before the swell, but SwellTicks get no StartTime (ie. they are zero). + // This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits. + // But for sample playback purposes they can be ignored as noise. AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); From ce1579f2fead9c2c84ff160dff5025ee41603998 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Jun 2023 14:05:02 +0900 Subject: [PATCH 0320/2100] Bind to `API.State` instead of `API.User` --- osu.Game/Overlays/Comments/CommentEditor.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index 8dbafc0ea8..05bdac5966 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -14,7 +14,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; using osuTK; using osuTK.Graphics; @@ -36,14 +35,14 @@ namespace osu.Game.Overlays.Comments protected TextBox TextBox { get; private set; } = null!; - protected readonly IBindable User = new Bindable(); - [Resolved] protected IAPIProvider API { get; private set; } = null!; [Resolved] private LoginOverlay? loginOverlay { get; set; } + private readonly IBindable apiState = new Bindable(); + /// /// Returns the text content of the main action button. /// When is , the text will apply to a button that posts a comment. @@ -162,14 +161,14 @@ namespace osu.Game.Overlays.Comments }); TextBox.OnCommit += (_, _) => commitButton.TriggerClick(); - User.BindTo(API.LocalUser); + apiState.BindTo(API.State); } protected override void LoadComplete() { base.LoadComplete(); Current.BindValueChanged(_ => updateCommitButtonState(), true); - User.BindValueChanged(_ => updateStateForLoggedIn(), true); + apiState.BindValueChanged(updateStateForLoggedIn, true); } protected abstract void OnCommit(string text); @@ -177,12 +176,14 @@ namespace osu.Game.Overlays.Comments private void updateCommitButtonState() => commitButton.Enabled.Value = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value); - private void updateStateForLoggedIn() + private void updateStateForLoggedIn(ValueChangedEvent state) { - TextBox.PlaceholderText = GetPlaceholderText(API.IsLoggedIn); - TextBox.ReadOnly = !API.IsLoggedIn; + bool isAvailable = state.NewValue > APIState.Offline; - if (API.IsLoggedIn) + TextBox.PlaceholderText = GetPlaceholderText(isAvailable); + TextBox.ReadOnly = !isAvailable; + + if (isAvailable) { commitButton.Show(); logInButton.Hide(); From 343271751add838bdd6047e5a8c6cfba0bf0e5b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Jun 2023 14:07:33 +0900 Subject: [PATCH 0321/2100] Add `Schedule` to ensure correct thread for UI code --- osu.Game/Overlays/Comments/CommentEditor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index 05bdac5966..02bcbb9d05 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -176,7 +176,7 @@ namespace osu.Game.Overlays.Comments private void updateCommitButtonState() => commitButton.Enabled.Value = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value); - private void updateStateForLoggedIn(ValueChangedEvent state) + private void updateStateForLoggedIn(ValueChangedEvent state) => Schedule(() => { bool isAvailable = state.NewValue > APIState.Offline; @@ -193,7 +193,7 @@ namespace osu.Game.Overlays.Comments commitButton.Hide(); logInButton.Show(); } - } + }); private partial class EditorTextBox : OsuTextBox { From 08b3c0cce0ba40f250fbc88b86076337b0462d92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Jun 2023 14:18:11 +0900 Subject: [PATCH 0322/2100] Change "floating fruits" mod to only apply adjustments to the playfield Avoids things like touch screen inputs also being flipped. Note that these adjustments can't be applied directly to the playfield due to how playfields are used in various rulesets (basically relying on the `PlayfieldAdjustContainer` to get things in the right place). Closes #24000. --- osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs | 6 +++--- osu.Game/Rulesets/UI/DrawableRuleset.cs | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs index e12181d051..dd6757eac9 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs @@ -21,10 +21,10 @@ namespace osu.Game.Rulesets.Catch.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - drawableRuleset.Anchor = Anchor.Centre; - drawableRuleset.Origin = Anchor.Centre; + drawableRuleset.PlayfieldAdjustmentContainer.Anchor = Anchor.Centre; + drawableRuleset.PlayfieldAdjustmentContainer.Origin = Anchor.Centre; - drawableRuleset.Scale = new Vector2(1, -1); + drawableRuleset.PlayfieldAdjustmentContainer.Scale = new Vector2(1, -1); } } } diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 4f22c0c617..cecd6cb26c 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -71,6 +71,12 @@ namespace osu.Game.Rulesets.UI private readonly AudioContainer audioContainer = new AudioContainer { RelativeSizeAxes = Axes.Both }; + /// + /// A container which encapsulates the and provides any adjustments to + /// ensure correct scale and position. + /// + public virtual PlayfieldAdjustmentContainer PlayfieldAdjustmentContainer { get; private set; } + public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both }; public override IFrameStableClock FrameStableClock => frameStabilityContainer; @@ -178,7 +184,7 @@ namespace osu.Game.Rulesets.UI audioContainer.WithChild(KeyBindingInputManager .WithChildren(new Drawable[] { - CreatePlayfieldAdjustmentContainer() + PlayfieldAdjustmentContainer = CreatePlayfieldAdjustmentContainer() .WithChild(Playfield), Overlays })), From 11a97e1bb88f39571333837385f442ceb21a7b47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Jun 2023 19:06:47 +0900 Subject: [PATCH 0323/2100] Move confirmation bypass implementation to `MainMenu` to allow for more correct logic --- .../Visual/Menus/TestSceneToolbar.cs | 2 + .../TestSceneFirstRunSetupOverlay.cs | 2 + osu.Game/OsuGame.cs | 14 +----- osu.Game/Overlays/INotificationOverlay.cs | 5 ++ osu.Game/Screens/Menu/MainMenu.cs | 47 ++++++++++++++----- 5 files changed, 45 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index 22c7bb64b2..471700988c 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -250,6 +250,8 @@ namespace osu.Game.Tests.Visual.Menus } public virtual IBindable UnreadCount => null; + + public bool HasOngoingOperations => false; } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs index 77ed97e3ed..0b0c29acf6 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs @@ -214,6 +214,8 @@ namespace osu.Game.Tests.Visual.UserInterface } public virtual IBindable UnreadCount => null; + + public bool HasOngoingOperations => false; } // interface mocks break hot reload, mocking this stub implementation instead works around it. diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 7bfe88f248..160dc26790 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -778,18 +778,8 @@ namespace osu.Game public override void AttemptExit() { - bool requiresConfirmationToExit = Notifications.HasOngoingOperations; - - PerformFromScreen(menu => - { - var mainMenu = ((MainMenu)menu); - - // The main menu exit implementation gives the user a chance to interrupt the exit process if needed. - if (requiresConfirmationToExit) - mainMenu.Exit(); - else - mainMenu.ExitWithoutConfirmation(); - }, new[] { typeof(MainMenu) }); + // The main menu exit implementation gives the user a chance to interrupt the exit process if needed. + PerformFromScreen(menu => menu.Exit(), new[] { typeof(MainMenu) }); } /// diff --git a/osu.Game/Overlays/INotificationOverlay.cs b/osu.Game/Overlays/INotificationOverlay.cs index b9ac466229..0d8f73a1d7 100644 --- a/osu.Game/Overlays/INotificationOverlay.cs +++ b/osu.Game/Overlays/INotificationOverlay.cs @@ -30,5 +30,10 @@ namespace osu.Game.Overlays /// Current number of unread notifications. /// IBindable UnreadCount { get; } + + /// + /// Whether there are any ongoing operations, such as imports or downloads. + /// + bool HasOngoingOperations { get; } } } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 998e36c2be..5078751823 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -53,6 +53,9 @@ namespace osu.Game.Screens.Menu [Resolved] private GameHost host { get; set; } + [Resolved] + private INotificationOverlay notifications { get; set; } + [Resolved] private MusicController musicController { get; set; } @@ -74,6 +77,9 @@ namespace osu.Game.Screens.Menu private ExitConfirmOverlay exitConfirmOverlay; + private bool exitConfirmedViaDialog; + private bool exitConfirmedViaHoldOrClick; + private ParallaxContainer buttonsContainer; private SongTicker songTicker; @@ -89,10 +95,8 @@ namespace osu.Game.Screens.Menu { Action = () => { - if (holdDelay.Value > 0) - confirmAndExit(); - else - this.Exit(); + exitConfirmedViaHoldOrClick = holdDelay.Value > 0; + this.Exit(); } }); } @@ -114,7 +118,11 @@ namespace osu.Game.Screens.Menu OnSolo = loadSoloSongSelect, OnMultiplayer = () => this.Push(new Multiplayer()), OnPlaylists = () => this.Push(new Playlists()), - OnExit = confirmAndExit, + OnExit = () => + { + exitConfirmedViaHoldOrClick = true; + this.Exit(); + } } } }, @@ -154,13 +162,11 @@ namespace osu.Game.Screens.Menu public void ReturnToOsuLogo() => Buttons.State = ButtonSystemState.Initial; - public void ExitWithoutConfirmation() => confirmAndExit(); - private void confirmAndExit() { - if (exitConfirmed) return; + if (exitConfirmedViaDialog) return; - exitConfirmed = true; + exitConfirmedViaDialog = true; performer?.PerformFromScreen(menu => menu.Exit()); } @@ -203,8 +209,6 @@ namespace osu.Game.Screens.Menu dialogOverlay?.Push(new StorageErrorDialog(osuStorage, osuStorage.Error)); } - private bool exitConfirmed; - protected override void LogoArriving(OsuLogo logo, bool resuming) { base.LogoArriving(logo, resuming); @@ -281,12 +285,29 @@ namespace osu.Game.Screens.Menu public override bool OnExiting(ScreenExitEvent e) { - if (!exitConfirmed && dialogOverlay != null) + bool requiresConfirmation = + // we need to have a dialog overlay to confirm in the first place. + dialogOverlay != null + // if the dialog has already displayed and been accepted by the user, we are good. + && !exitConfirmedViaDialog + // Only require confirmation if there is either an ongoing operation or the user exited via a non-hold escape press. + && (notifications.HasOngoingOperations || !exitConfirmedViaHoldOrClick); + + if (requiresConfirmation) { if (dialogOverlay.CurrentDialog is ConfirmExitDialog exitDialog) exitDialog.PerformOkAction(); else - dialogOverlay.Push(new ConfirmExitDialog(confirmAndExit, () => exitConfirmOverlay.Abort())); + { + dialogOverlay.Push(new ConfirmExitDialog(() => + { + exitConfirmedViaDialog = true; + this.Exit(); + }, () => + { + exitConfirmOverlay.Abort(); + })); + } return true; } From 6df617d5366b3a8f15ecaa03c9ce52e1d97f71eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Jun 2023 14:46:38 +0900 Subject: [PATCH 0324/2100] Rename `ExitConfirmOverlay` to be more explicit about purpose --- .../Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs | 4 ++-- .../{ExitConfirmOverlay.cs => HoldToExitGameOverlay.cs} | 6 ++---- osu.Game/Screens/Menu/MainMenu.cs | 8 ++++---- 3 files changed, 8 insertions(+), 10 deletions(-) rename osu.Game/Screens/Menu/{ExitConfirmOverlay.cs => HoldToExitGameOverlay.cs} (86%) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs index 801bef62c8..9c29f17382 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.UserInterface Alpha = 0, }; - var overlay = new TestHoldToConfirmOverlay + var overlay = new TestHoldToExitGameOverlay { Action = () => { @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("wait until fired again", () => overlay.Fired); } - private partial class TestHoldToConfirmOverlay : ExitConfirmOverlay + private partial class TestHoldToExitGameOverlay : HoldToExitGameOverlay { public void Begin() => BeginConfirm(); } diff --git a/osu.Game/Screens/Menu/ExitConfirmOverlay.cs b/osu.Game/Screens/Menu/HoldToExitGameOverlay.cs similarity index 86% rename from osu.Game/Screens/Menu/ExitConfirmOverlay.cs rename to osu.Game/Screens/Menu/HoldToExitGameOverlay.cs index bc2f6ea00f..a8f4913368 100644 --- a/osu.Game/Screens/Menu/ExitConfirmOverlay.cs +++ b/osu.Game/Screens/Menu/HoldToExitGameOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; @@ -10,13 +8,13 @@ using osu.Game.Overlays; namespace osu.Game.Screens.Menu { - public partial class ExitConfirmOverlay : HoldToConfirmOverlay, IKeyBindingHandler + public partial class HoldToExitGameOverlay : HoldToConfirmOverlay, IKeyBindingHandler { protected override bool AllowMultipleFires => true; public void Abort() => AbortConfirm(); - public ExitConfirmOverlay() + public HoldToExitGameOverlay() : base(0.7f) { } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 5078751823..2946425998 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.Menu private Bindable holdDelay; private Bindable loginDisplayed; - private ExitConfirmOverlay exitConfirmOverlay; + private HoldToExitGameOverlay holdToExitGameOverlay; private bool exitConfirmedViaDialog; private bool exitConfirmedViaHoldOrClick; @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Menu if (host.CanExit) { - AddInternal(exitConfirmOverlay = new ExitConfirmOverlay + AddInternal(holdToExitGameOverlay = new HoldToExitGameOverlay { Action = () => { @@ -133,7 +133,7 @@ namespace osu.Game.Screens.Menu Origin = Anchor.TopRight, Margin = new MarginPadding { Right = 15, Top = 5 } }, - exitConfirmOverlay?.CreateProxy() ?? Empty() + holdToExitGameOverlay?.CreateProxy() ?? Empty() }); Buttons.StateChanged += state => @@ -305,7 +305,7 @@ namespace osu.Game.Screens.Menu this.Exit(); }, () => { - exitConfirmOverlay.Abort(); + holdToExitGameOverlay.Abort(); })); } From 20aedc82ac674b441ee01c31d5b76cf32232564d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Jun 2023 14:47:32 +0900 Subject: [PATCH 0325/2100] Remove unused code --- osu.Game/Screens/Menu/MainMenu.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 2946425998..f55c44d427 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -157,19 +157,8 @@ namespace osu.Game.Screens.Menu preloadSongSelect(); } - [Resolved(canBeNull: true)] - private IPerformFromScreenRunner performer { get; set; } - public void ReturnToOsuLogo() => Buttons.State = ButtonSystemState.Initial; - private void confirmAndExit() - { - if (exitConfirmedViaDialog) return; - - exitConfirmedViaDialog = true; - performer?.PerformFromScreen(menu => menu.Exit()); - } - private void preloadSongSelect() { if (songSelect == null) From 7fa07805b0bffb300ff1e071868294d3fb7d16cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Jun 2023 14:58:19 +0900 Subject: [PATCH 0326/2100] Expose all notifications from `INotificationOverlay` Also fixes `HasOngoingOperations` not actually working. --- osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs | 3 ++- .../UserInterface/TestSceneFirstRunSetupOverlay.cs | 2 +- osu.Game/Overlays/INotificationOverlay.cs | 14 +++++++++++++- osu.Game/Overlays/NotificationOverlay.cs | 4 +++- osu.Game/Overlays/NotificationOverlayToastTray.cs | 5 +++++ .../Overlays/Notifications/NotificationSection.cs | 5 +++++ 6 files changed, 29 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index 471700988c..ce9f80a84f 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using Moq; @@ -251,7 +252,7 @@ namespace osu.Game.Tests.Visual.Menus public virtual IBindable UnreadCount => null; - public bool HasOngoingOperations => false; + public IEnumerable AllNotifications => Enumerable.Empty(); } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs index 0b0c29acf6..9275f9755f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs @@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.UserInterface public virtual IBindable UnreadCount => null; - public bool HasOngoingOperations => false; + public IEnumerable AllNotifications => Enumerable.Empty(); } // interface mocks break hot reload, mocking this stub implementation instead works around it. diff --git a/osu.Game/Overlays/INotificationOverlay.cs b/osu.Game/Overlays/INotificationOverlay.cs index 0d8f73a1d7..1dd6dd2c6c 100644 --- a/osu.Game/Overlays/INotificationOverlay.cs +++ b/osu.Game/Overlays/INotificationOverlay.cs @@ -3,6 +3,8 @@ #nullable disable +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Overlays.Notifications; @@ -34,6 +36,16 @@ namespace osu.Game.Overlays /// /// Whether there are any ongoing operations, such as imports or downloads. /// - bool HasOngoingOperations { get; } + public bool HasOngoingOperations => OngoingOperations.Any(); + + /// + /// All current displayed notifications, whether in the toast tray or a section. + /// + IEnumerable AllNotifications { get; } + + /// + /// All ongoing operations (ie. any not in a completed state). + /// + public IEnumerable OngoingOperations => AllNotifications.OfType().Where(p => p.State != ProgressNotificationState.Completed); } } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index c53b6b667c..6ef93fac9e 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.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 osu.Framework.Allocation; using osu.Framework.Audio; @@ -33,7 +34,8 @@ namespace osu.Game.Overlays public const float TRANSITION_LENGTH = 600; - public bool HasOngoingOperations => sections.Any(s => s.Children.OfType().Any()); + public IEnumerable AllNotifications => + toastTray.Notifications.Concat(sections.SelectMany(s => s.Notifications)); private FlowContainer sections = null!; diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index 7a793ee092..0ebaff9437 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -28,6 +28,11 @@ namespace osu.Game.Overlays public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => toastFlow.ReceivePositionalInputAt(screenSpacePos); + /// + /// All notifications currently being displayed by the toast tray. + /// + public IEnumerable Notifications => toastFlow; + public bool IsDisplayingToasts => toastFlow.Count > 0; private FillFlowContainer toastFlow = null!; diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index de4c72e473..4e28ade802 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -19,6 +19,11 @@ namespace osu.Game.Overlays.Notifications { public partial class NotificationSection : AlwaysUpdateFillFlowContainer { + /// + /// All notifications currently being displayed in this section. + /// + public IEnumerable Notifications => notifications; + private OsuSpriteText countDrawable = null!; private FlowContainer notifications = null!; From 693b7c99067c64f6daa10da3300041a0be0a91b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Jun 2023 15:19:47 +0900 Subject: [PATCH 0327/2100] Reorganise resolved fields in `MainMenu` --- osu.Game/Screens/Menu/MainMenu.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index f55c44d427..f34a1954a5 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -59,12 +59,15 @@ namespace osu.Game.Screens.Menu [Resolved] private MusicController musicController { get; set; } - [Resolved(canBeNull: true)] - private LoginOverlay login { get; set; } - [Resolved] private IAPIProvider api { get; set; } + [Resolved] + private Storage storage { get; set; } + + [Resolved(canBeNull: true)] + private LoginOverlay login { get; set; } + [Resolved(canBeNull: true)] private IDialogOverlay dialogOverlay { get; set; } @@ -174,9 +177,6 @@ namespace osu.Game.Screens.Menu return s; } - [Resolved] - private Storage storage { get; set; } - public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); From f66b787b12d5f0a449d3d0c1f8bf497d15c7a216 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Jun 2023 15:20:19 +0900 Subject: [PATCH 0328/2100] Show ongoing operations in exit confirmation dialog Also changes the button to a dangerous button, forcing user acknowledgement --- osu.Game/Screens/Menu/ConfirmExitDialog.cs | 63 ++++++++++++++++++---- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Menu/ConfirmExitDialog.cs b/osu.Game/Screens/Menu/ConfirmExitDialog.cs index 4906232d21..fb22f7eff8 100644 --- a/osu.Game/Screens/Menu/ConfirmExitDialog.cs +++ b/osu.Game/Screens/Menu/ConfirmExitDialog.cs @@ -2,38 +2,79 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; +using osu.Game.Overlays; using osu.Game.Overlays.Dialog; namespace osu.Game.Screens.Menu { public partial class ConfirmExitDialog : PopupDialog { + private readonly Action onConfirm; + private readonly Action? onCancel; + /// /// Construct a new exit confirmation dialog. /// /// An action to perform on confirmation. /// An optional action to perform on cancel. public ConfirmExitDialog(Action onConfirm, Action? onCancel = null) + { + this.onConfirm = onConfirm; + this.onCancel = onCancel; + } + + [BackgroundDependencyLoader] + private void load(INotificationOverlay notifications) { HeaderText = "Are you sure you want to exit osu!?"; - BodyText = "Last chance to turn back"; Icon = FontAwesome.Solid.ExclamationTriangle; - Buttons = new PopupDialogButton[] + if (notifications.HasOngoingOperations) { - new PopupDialogOkButton + string text = "There are currently some background operations which will be aborted if you continue:\n\n"; + + foreach (var n in notifications.OngoingOperations) + text += $"{n.Text} ({n.Progress:0%})\n"; + + text += "\nLast chance to turn back"; + + BodyText = text; + + Buttons = new PopupDialogButton[] { - Text = @"Let me out!", - Action = onConfirm - }, - new PopupDialogCancelButton + new PopupDialogDangerousButton + { + Text = @"Let me out!", + Action = onConfirm + }, + new PopupDialogCancelButton + { + Text = @"Cancel", + Action = onCancel + }, + }; + } + else + { + BodyText = "Last chance to turn back"; + + Buttons = new PopupDialogButton[] { - Text = @"Just a little more...", - Action = onCancel - }, - }; + new PopupDialogOkButton + { + Text = @"Let me out!", + Action = onConfirm + }, + new PopupDialogCancelButton + { + Text = @"Just a little more...", + Action = onCancel + }, + }; + } } } } From a76037b6433951f8e5dbe5ab7d6534edb5f37a4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Jun 2023 15:30:21 +0900 Subject: [PATCH 0329/2100] Add test coverage of confirm-for-operations --- .../Navigation/TestSceneScreenNavigation.cs | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 18aef99ccd..c3b668f591 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -22,6 +22,7 @@ using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.Mods; +using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Toolbar; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; @@ -683,6 +684,44 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("center cursor", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre)); } + [Test] + public void TestExitWithOperationInProgress() + { + AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); + + ProgressNotification progressNotification = null!; + + AddStep("start ongoing operation", () => + { + progressNotification = new ProgressNotification + { + Text = "Something is still running", + Progress = 0.5f, + State = ProgressNotificationState.Active, + }; + Game.Notifications.Post(progressNotification); + }); + + AddStep("Hold escape", () => InputManager.PressKey(Key.Escape)); + AddUntilStep("confirmation dialog shown", () => Game.ChildrenOfType().Single().CurrentDialog is ConfirmExitDialog); + AddStep("Release escape", () => InputManager.ReleaseKey(Key.Escape)); + + AddStep("cancel exit", () => InputManager.Key(Key.Escape)); + AddAssert("dialog dismissed", () => Game.ChildrenOfType().Single().CurrentDialog == null); + + AddStep("complete operation", () => + { + progressNotification.Progress = 100; + progressNotification.State = ProgressNotificationState.Completed; + }); + + AddStep("Hold escape", () => InputManager.PressKey(Key.Escape)); + AddUntilStep("Wait for intro", () => Game.ScreenStack.CurrentScreen is IntroScreen); + AddStep("Release escape", () => InputManager.ReleaseKey(Key.Escape)); + + AddUntilStep("Wait for game exit", () => Game.ScreenStack.CurrentScreen == null); + } + [Test] public void TestExitGameFromSongSelect() { @@ -699,7 +738,7 @@ namespace osu.Game.Tests.Visual.Navigation } [Test] - public void TestRapidBackButtonExit() + public void TestExitWithHoldDisabled() { AddStep("set hold delay to 0", () => Game.LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0)); @@ -711,7 +750,7 @@ namespace osu.Game.Tests.Visual.Navigation pushEscape(); - AddAssert("exit dialog is shown", () => Game.Dependencies.Get().CurrentDialog != null); + AddAssert("exit dialog is shown", () => Game.Dependencies.Get().CurrentDialog is ConfirmExitDialog); } private Func playToResults() From 87447f41d0f4fb9a527aad577c1b26586fff1a74 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 24 Jun 2023 00:58:45 +0900 Subject: [PATCH 0330/2100] Fix incorrect calculation of difficulty --- .../Difficulty/CatchScoreV1Processor.cs | 10 +++++++++- .../Difficulty/OsuScoreV1Processor.cs | 10 +++++++++- .../Difficulty/TaikoScoreV1Processor.cs | 10 +++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs index 3f0ac7a760..be48763845 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs @@ -64,11 +64,19 @@ namespace osu.Game.Rulesets.Catch.Difficulty int objectCount = countNormal + countSlider + countSpinner; + int drainLength = 0; + + if (baseBeatmap.HitObjects.Count > 0) + { + int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum(); + drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000; + } + int difficultyPeppyStars = (int)Math.Round( (baseBeatmap.Difficulty.DrainRate + baseBeatmap.Difficulty.OverallDifficulty + baseBeatmap.Difficulty.CircleSize - + Math.Clamp(objectCount / baseBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); + + Math.Clamp(objectCount / drainLength * 8, 0, 16)) / 38 * 5); scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs index 28d029b73a..e8231794e0 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs @@ -67,11 +67,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty int objectCount = countNormal + countSlider + countSpinner; + int drainLength = 0; + + if (baseBeatmap.HitObjects.Count > 0) + { + int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum(); + drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000; + } + int difficultyPeppyStars = (int)Math.Round( (baseBeatmap.Difficulty.DrainRate + baseBeatmap.Difficulty.OverallDifficulty + baseBeatmap.Difficulty.CircleSize - + Math.Clamp(objectCount / baseBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); + + Math.Clamp(objectCount / drainLength * 8, 0, 16)) / 38 * 5); scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs index 23ff9585e8..f01ca74f4a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs @@ -70,11 +70,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty int objectCount = countNormal + countSlider + countSpinner; + int drainLength = 0; + + if (baseBeatmap.HitObjects.Count > 0) + { + int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum(); + drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000; + } + difficultyPeppyStars = (int)Math.Round( (baseBeatmap.Difficulty.DrainRate + baseBeatmap.Difficulty.OverallDifficulty + baseBeatmap.Difficulty.CircleSize - + Math.Clamp(objectCount / baseBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); + + Math.Clamp(objectCount / drainLength * 8, 0, 16)) / 38 * 5); modMultiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); From 0ab0c52ad577b3e7b406d09fa6056a56ff997c3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Jun 2023 01:37:25 +0900 Subject: [PATCH 0331/2100] Automated pass --- osu.Android/GameplayScreenRotationLocker.cs | 4 +--- osu.Android/OsuGameActivity.cs | 2 -- osu.Android/OsuGameAndroid.cs | 2 -- osu.Android/Properties/AssemblyInfo.cs | 4 +--- .../MainActivity.cs | 2 -- .../CatchBeatmapConversionTest.cs | 2 -- .../CatchDifficultyCalculatorTest.cs | 4 +--- .../CatchLegacyModConversionTest.cs | 2 -- .../CatchSkinColourDecodingTest.cs | 4 +--- .../CatchSkinnableTestScene.cs | 4 +--- .../Editor/CatchEditorTestSceneContainer.cs | 4 +--- .../CatchSelectionBlueprintTestScene.cs | 4 +--- ...TestSceneBananaShowerPlacementBlueprint.cs | 4 +--- .../Editor/TestSceneCatchDistanceSnapGrid.cs | 4 +--- .../Editor/TestSceneEditor.cs | 4 +--- .../TestSceneFruitPlacementBlueprint.cs | 4 +--- .../JuiceStreamPathTest.cs | 4 +--- .../TestSceneAutoJuiceStream.cs | 2 -- .../TestSceneBananaShower.cs | 2 -- .../TestSceneCatchModHidden.cs | 4 +--- .../TestSceneCatchPlayer.cs | 2 -- .../TestSceneCatchPlayerLegacySkin.cs | 4 +--- .../TestSceneCatchReplay.cs | 4 +--- .../TestSceneCatchStacker.cs | 2 -- .../TestSceneDrawableHitObjectsHidden.cs | 4 +--- .../TestSceneFruitObjects.cs | 2 -- .../TestSceneFruitRandomness.cs | 4 +--- .../TestSceneFruitVisualChange.cs | 2 -- .../TestSceneHyperDash.cs | 2 -- .../TestSceneJuiceStream.cs | 4 +--- .../TestSceneLegacyBeatmapSkin.cs | 6 ++--- .../MainActivity.cs | 2 -- .../ManiaSelectionBlueprintTestScene.cs | 4 +--- .../Editor/TestSceneEditor.cs | 2 -- .../TestSceneHoldNotePlacementBlueprint.cs | 4 +--- .../TestSceneHoldNoteSelectionBlueprint.cs | 4 +--- .../Editor/TestSceneManiaBeatSnapGrid.cs | 4 +--- .../Editor/TestSceneManiaComposeScreen.cs | 6 ++--- .../Editor/TestSceneNoteSelectionBlueprint.cs | 4 +--- .../ManiaDifficultyCalculatorTest.cs | 4 +--- .../ManiaInputTestScene.cs | 2 -- .../ManiaLegacyModConversionTest.cs | 2 -- .../ManiaLegacyReplayTest.cs | 4 +--- .../ManiaSpecialColumnTest.cs | 4 +--- .../Skinning/ColumnTestContainer.cs | 4 +--- .../Skinning/ManiaHitObjectTestScene.cs | 4 +--- .../Skinning/ManiaSkinnableTestScene.cs | 4 +--- .../Skinning/TestSceneColumnBackground.cs | 4 +--- .../Skinning/TestSceneColumnHitObjectArea.cs | 4 +--- .../Skinning/TestSceneDrawableJudgement.cs | 4 +--- .../Skinning/TestSceneHitExplosion.cs | 4 +--- .../Skinning/TestSceneHoldNote.cs | 4 +--- .../Skinning/TestSceneNote.cs | 4 +--- .../Skinning/TestScenePlayfield.cs | 4 +--- .../Skinning/TestSceneStage.cs | 4 +--- .../Skinning/TestSceneStageBackground.cs | 4 +--- .../TestSceneAutoGeneration.cs | 4 +--- .../TestSceneColumn.cs | 2 -- .../TestSceneManiaHitObjectSamples.cs | 4 +--- .../TestSceneManiaPlayer.cs | 4 +--- .../TestScenePlayfieldCoveringContainer.cs | 4 +--- .../Beatmaps/ManiaBeatmap.cs | 2 -- .../Legacy/EndTimeObjectPatternGenerator.cs | 2 -- .../Legacy/HitObjectPatternGenerator.cs | 4 +--- .../Beatmaps/Patterns/Legacy/PatternType.cs | 2 -- .../Beatmaps/Patterns/PatternGenerator.cs | 2 -- .../Beatmaps/StageDefinition.cs | 2 -- .../Difficulty/ManiaDifficultyCalculator.cs | 4 +--- .../Difficulty/ManiaPerformanceAttributes.cs | 2 -- .../Difficulty/ManiaPerformanceCalculator.cs | 2 -- .../Preprocessing/ManiaDifficultyHitObject.cs | 4 +--- .../Difficulty/Skills/Strain.cs | 4 +--- .../DualStageVariantGenerator.cs | 4 +--- .../Blueprints/Components/EditBodyPiece.cs | 4 +--- .../Blueprints/Components/EditNotePiece.cs | 4 +--- .../Blueprints/HoldNotePlacementBlueprint.cs | 6 ++--- .../Blueprints/ManiaSelectionBlueprint.cs | 8 +++---- .../Edit/Blueprints/NotePlacementBlueprint.cs | 4 +--- .../Edit/Blueprints/NoteSelectionBlueprint.cs | 2 -- .../Edit/DrawableManiaEditorRuleset.cs | 2 -- .../Edit/HoldNoteCompositionTool.cs | 4 +--- .../Edit/ManiaBeatSnapGrid.cs | 12 +++++----- .../Edit/ManiaBlueprintContainer.cs | 4 +--- .../Edit/ManiaEditorPlayfield.cs | 2 -- .../Edit/ManiaSelectionHandler.cs | 6 ++--- .../Edit/NoteCompositionTool.cs | 4 +--- .../Judgements/HoldNoteTickJudgement.cs | 4 +--- .../Judgements/ManiaJudgement.cs | 2 -- osu.Game.Rulesets.Mania/ManiaInputManager.cs | 2 -- .../Objects/HoldNoteTick.cs | 4 +--- .../Objects/ManiaHitObject.cs | 2 -- osu.Game.Rulesets.Mania/Objects/Note.cs | 2 -- osu.Game.Rulesets.Mania/Objects/TailNote.cs | 4 +--- .../Properties/AssemblyInfo.cs | 2 -- .../Scoring/ManiaHealthProcessor.cs | 4 +--- .../Scoring/ManiaHitWindows.cs | 2 -- .../SingleStageVariantGenerator.cs | 4 +--- .../UI/Components/ColumnHitObjectArea.cs | 2 -- .../UI/Components/DefaultStageBackground.cs | 4 +--- .../UI/Components/HitObjectArea.cs | 4 +--- osu.Game.Rulesets.Mania/UI/IHitExplosion.cs | 4 +--- .../UI/ManiaPlayfieldAdjustmentContainer.cs | 4 +--- .../UI/ManiaReplayRecorder.cs | 4 +--- .../UI/OrderedHitPolicy.cs | 4 +--- .../UI/PlayfieldCoveringWrapper.cs | 4 +--- .../MainActivity.cs | 2 -- .../TestSceneHitCirclePlacementBlueprint.cs | 4 +--- .../Editor/TestSceneOsuEditor.cs | 2 -- .../Editor/TestSceneOsuEditorGrids.cs | 4 +--- .../TestSceneOsuEditorSelectInvalidPath.cs | 4 +--- .../TestSceneSpinnerPlacementBlueprint.cs | 4 +--- .../TestSceneSpinnerSelectionBlueprint.cs | 4 +--- .../OsuBeatmapConversionTest.cs | 2 -- .../OsuDifficultyCalculatorTest.cs | 4 +--- .../OsuLegacyModConversionTest.cs | 2 -- osu.Game.Rulesets.Osu.Tests/StackingTest.cs | 4 +--- .../TestPlayfieldBorder.cs | 4 +--- .../TestSceneDrawableJudgement.cs | 4 +--- .../TestSceneHitCircle.cs | 4 +--- .../TestSceneHitCircleComboChange.cs | 4 +--- .../TestSceneHitCircleHidden.cs | 2 -- .../TestSceneHitCircleLongCombo.cs | 4 +--- .../TestSceneMissHitWindowJudgements.cs | 4 +--- .../TestSceneNoSpinnerStacking.cs | 2 -- .../TestSceneOsuHitObjectSamples.cs | 4 +--- .../TestSceneOsuPlayer.cs | 4 +--- .../TestSceneResumeOverlay.cs | 4 +--- .../TestSceneShaking.cs | 4 +--- .../TestSceneSliderComboChange.cs | 4 +--- .../TestSceneSliderHidden.cs | 2 -- .../TestSceneSpinnerHidden.cs | 2 -- osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs | 2 -- .../Beatmaps/OsuBeatmapConverter.cs | 2 -- .../Beatmaps/OsuBeatmapProcessor.cs | 2 -- .../Difficulty/Evaluators/AimEvaluator.cs | 4 +--- .../Evaluators/FlashlightEvaluator.cs | 4 +--- .../Difficulty/Evaluators/RhythmEvaluator.cs | 4 +--- .../Difficulty/Evaluators/SpeedEvaluator.cs | 4 +--- .../Difficulty/OsuPerformanceAttributes.cs | 2 -- .../Difficulty/OsuPerformanceCalculator.cs | 4 +--- .../Preprocessing/OsuDifficultyHitObject.cs | 4 +--- .../Difficulty/Skills/Aim.cs | 2 -- .../Difficulty/Skills/Flashlight.cs | 2 -- .../Difficulty/Skills/OsuStrainSkill.cs | 2 -- .../Difficulty/Skills/Speed.cs | 2 -- .../Edit/Blueprints/BlueprintPiece.cs | 4 +--- .../HitCircles/Components/HitCirclePiece.cs | 4 +--- .../HitCircles/HitCirclePlacementBlueprint.cs | 4 +--- .../HitCircles/HitCircleSelectionBlueprint.cs | 2 -- .../Edit/Blueprints/OsuSelectionBlueprint.cs | 6 ++--- .../Sliders/Components/SliderBodyPiece.cs | 4 +--- .../Blueprints/Sliders/SliderCircleOverlay.cs | 4 +--- .../Spinners/Components/SpinnerPiece.cs | 4 +--- .../Spinners/SpinnerSelectionBlueprint.cs | 4 +--- .../Edit/HitCircleCompositionTool.cs | 4 +--- .../Edit/OsuBeatmapVerifier.cs | 4 +--- .../Edit/OsuBlueprintContainer.cs | 4 +--- .../Edit/OsuRectangularPositionSnapGrid.cs | 6 ++--- .../Edit/SliderCompositionTool.cs | 4 +--- .../Edit/SpinnerCompositionTool.cs | 4 +--- .../Judgements/ComboResult.cs | 4 +--- .../Judgements/OsuHitCircleJudgementResult.cs | 4 +--- .../Judgements/OsuIgnoreJudgement.cs | 4 +--- .../Judgements/OsuJudgement.cs | 2 -- .../Judgements/OsuJudgementResult.cs | 4 +--- .../Judgements/OsuSpinnerJudgementResult.cs | 4 +--- .../Judgements/SliderTickJudgement.cs | 4 +--- .../Drawables/Connections/FollowPoint.cs | 2 -- .../Drawables/DrawableSpinnerBonusTick.cs | 4 +--- .../Objects/Drawables/ITrackSnaking.cs | 2 -- osu.Game.Rulesets.Osu/Objects/HitCircle.cs | 2 -- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 2 -- .../Objects/SliderEndCircle.cs | 2 -- .../Objects/SliderHeadCircle.cs | 4 +--- osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs | 2 -- .../Objects/SliderTailCircle.cs | 4 +--- osu.Game.Rulesets.Osu/Objects/SliderTick.cs | 2 -- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 2 -- .../Objects/SpinnerBonusTick.cs | 4 +--- osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs | 4 +--- .../Properties/AssemblyInfo.cs | 2 -- osu.Game.Rulesets.Osu/UI/IHitPolicy.cs | 4 +--- .../UI/OsuPlayfieldAdjustmentContainer.cs | 4 +--- osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs | 4 +--- .../Utils/OsuHitObjectGenerationUtils.cs | 4 +--- .../MainActivity.cs | 2 -- .../DrawableTestHit.cs | 4 +--- .../DrawableTestStrongHit.cs | 4 +--- .../Editor/TestSceneEditor.cs | 4 +--- .../Editor/TestSceneTaikoEditorSaving.cs | 4 +--- .../Editor/TestSceneTaikoHitObjectComposer.cs | 4 +--- .../Skinning/TaikoSkinnableTestScene.cs | 4 +--- .../Skinning/TestSceneDrawableBarLine.cs | 4 +--- .../Skinning/TestSceneDrawableDrumRoll.cs | 4 +--- .../Skinning/TestSceneDrawableHit.cs | 4 +--- .../Skinning/TestSceneHitExplosion.cs | 4 +--- .../Skinning/TestSceneInputDrum.cs | 4 +--- .../Skinning/TestSceneKiaiHitExplosion.cs | 4 +--- .../Skinning/TestSceneTaikoPlayfield.cs | 4 +--- .../Skinning/TestSceneTaikoScroller.cs | 4 +--- .../TaikoBeatmapConversionTest.cs | 2 -- .../TaikoDifficultyCalculatorTest.cs | 4 +--- .../TaikoLegacyModConversionTest.cs | 2 -- .../TestSceneBarLineApplication.cs | 4 +--- .../TestSceneDrumRollApplication.cs | 4 +--- .../TestSceneHitApplication.cs | 4 +--- .../TestSceneHits.cs | 2 -- .../TestSceneSampleOutput.cs | 4 +--- .../TestSceneTaikoPlayer.cs | 4 +--- .../TestSceneTaikoSuddenDeath.cs | 4 +--- .../Beatmaps/TaikoBeatmap.cs | 2 -- .../Beatmaps/TaikoBeatmapConverter.cs | 2 -- .../Difficulty/Skills/Rhythm.cs | 2 -- .../Difficulty/Skills/Stamina.cs | 2 -- .../Difficulty/TaikoDifficultyCalculator.cs | 4 +--- .../Difficulty/TaikoPerformanceAttributes.cs | 2 -- .../Difficulty/TaikoPerformanceCalculator.cs | 2 -- .../Blueprints/DrumRollPlacementBlueprint.cs | 4 +--- .../Edit/Blueprints/HitPiece.cs | 4 +--- .../Edit/Blueprints/LengthPiece.cs | 4 +--- .../Blueprints/SwellPlacementBlueprint.cs | 4 +--- .../Blueprints/TaikoSelectionBlueprint.cs | 4 +--- .../Edit/DrumRollCompositionTool.cs | 4 +--- .../Edit/HitCompositionTool.cs | 4 +--- .../Edit/SwellCompositionTool.cs | 4 +--- .../Edit/TaikoBlueprintContainer.cs | 4 +--- .../Edit/TaikoSelectionHandler.cs | 4 +--- .../Judgements/TaikoDrumRollTickJudgement.cs | 2 -- .../Judgements/TaikoJudgement.cs | 2 -- .../Judgements/TaikoStrongJudgement.cs | 2 -- .../Judgements/TaikoSwellJudgement.cs | 2 -- osu.Game.Rulesets.Taiko/Objects/BarLine.cs | 2 -- .../Objects/Drawables/DrawableFlyingHit.cs | 4 +--- .../Drawables/DrawableStrongNestedHit.cs | 4 +--- .../Objects/DrumRollTick.cs | 2 -- osu.Game.Rulesets.Taiko/Objects/Hit.cs | 2 -- osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs | 2 -- .../Objects/StrongNestedHitObject.cs | 4 +--- osu.Game.Rulesets.Taiko/Objects/Swell.cs | 2 -- osu.Game.Rulesets.Taiko/Objects/SwellTick.cs | 4 +--- .../Objects/TaikoHitObject.cs | 2 -- .../Objects/TaikoStrongableHitObject.cs | 4 +--- .../Properties/AssemblyInfo.cs | 2 -- .../Scoring/TaikoHealthProcessor.cs | 4 +--- .../Scoring/TaikoHitWindows.cs | 2 -- osu.Game.Rulesets.Taiko/TaikoInputManager.cs | 2 -- osu.Game.Tests.Android/MainActivity.cs | 2 -- .../Formats/LegacyBeatmapEncoderTest.cs | 4 +--- .../Beatmaps/Formats/LegacyDecoderTest.cs | 4 +--- .../Beatmaps/IO/LineBufferedReaderTest.cs | 2 -- .../Beatmaps/IO/OszArchiveReaderTest.cs | 2 -- .../Beatmaps/SliderEventGenerationTest.cs | 4 +--- .../Beatmaps/ToStringFormattingTest.cs | 4 +--- .../Collections/IO/ImportCollectionsTest.cs | 4 +--- .../Database/LegacyBeatmapImporterTest.cs | 4 +--- .../Gameplay/TestSceneHitObjectSamples.cs | 4 +--- .../Gameplay/TestSceneScoreProcessor.cs | 4 +--- osu.Game.Tests/ImportTest.cs | 4 +--- .../Input/ConfineMouseTrackerTest.cs | 6 ++--- .../BeatmapMetadataRomanisationTest.cs | 2 -- .../NonVisual/BarLineGeneratorTest.cs | 4 +--- .../NonVisual/ClosestBeatDivisorTest.cs | 4 +--- .../NonVisual/ControlPointInfoTest.cs | 4 +--- ...DifficultyAdjustmentModCombinationsTest.cs | 2 -- osu.Game.Tests/NonVisual/FormatUtilsTest.cs | 4 +--- .../StatefulMultiplayerClientTest.cs | 4 +--- osu.Game.Tests/NonVisual/PeriodTrackerTest.cs | 4 +--- .../NonVisual/RulesetInfoOrderingTest.cs | 4 +--- osu.Game.Tests/NonVisual/ScoreInfoTest.cs | 4 +--- .../NonVisual/TimeDisplayExtensionTest.cs | 4 +--- .../Online/Chat/MessageNotifierTest.cs | 4 +--- ...TestMultiplayerMessagePackSerialization.cs | 4 +--- .../TestSoloScoreInfoJsonSerialization.cs | 4 +--- .../OnlinePlay/PlaylistExtensionsTest.cs | 4 +--- osu.Game.Tests/Scores/IO/TestScoreEquality.cs | 4 +--- osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 4 +--- .../Skins/LegacyManiaSkinDecoderTest.cs | 4 +--- .../TestSceneBeatmapCardDifficultyList.cs | 4 +--- .../TestSceneBeatmapSetOnlineStatusPill.cs | 4 +--- .../Colours/TestSceneStarDifficultyColours.cs | 6 ++--- .../Editing/TestSceneBlueprintOrdering.cs | 4 +--- .../TestSceneEditorComposeRadioButtons.cs | 4 +--- .../Visual/Editing/TestSceneEditorMenuBar.cs | 4 +--- .../Editing/TestSceneEditorScreenModes.cs | 4 +--- .../Visual/Editing/TestSceneEditorSeeking.cs | 4 +--- .../Editing/TestSceneEditorSummaryTimeline.cs | 4 +--- ...ceneHitObjectDifficultyPointAdjustments.cs | 4 +--- .../TestSceneHitObjectSampleAdjustments.cs | 4 +--- .../Visual/Editing/TestSceneSetupScreen.cs | 4 +--- .../TestSceneTimelineBlueprintContainer.cs | 4 +--- .../TestSceneTimelineHitObjectBlueprint.cs | 2 -- .../Editing/TestSceneTimelineTickDisplay.cs | 4 +--- .../Visual/Editing/TestSceneTimelineZoom.cs | 4 +--- .../Visual/Gameplay/OsuPlayerTestScene.cs | 4 +--- .../Visual/Gameplay/TestSceneFailAnimation.cs | 4 +--- .../Visual/Gameplay/TestSceneFailJudgement.cs | 4 +--- .../Visual/Gameplay/TestSceneKeyCounter.cs | 2 -- .../Visual/Gameplay/TestSceneMedalOverlay.cs | 4 +--- .../TestSceneNightcoreBeatContainer.cs | 4 +--- .../TestSceneSkinEditorComponentsList.cs | 4 +--- .../TestSceneSkinnableAccuracyCounter.cs | 4 +--- .../TestSceneSkinnableComboCounter.cs | 4 +--- .../TestSceneSkinnableHealthDisplay.cs | 4 +--- .../TestSceneSkinnableScoreCounter.cs | 4 +--- .../Visual/Gameplay/TestSceneSpectatorHost.cs | 4 +--- .../Visual/Gameplay/TestSceneStarCounter.cs | 4 +--- .../Visual/Gameplay/TestSceneUnknownMod.cs | 4 +--- .../Visual/Menus/TestSceneDisclaimer.cs | 4 +--- .../Visual/Menus/TestSceneIntroCircles.cs | 2 -- .../Visual/Menus/TestSceneIntroTriangles.cs | 4 +--- .../Visual/Menus/TestSceneIntroWelcome.cs | 2 -- .../Visual/Menus/TestSceneSideOverlays.cs | 4 +--- .../Visual/Menus/TestSceneSongTicker.cs | 4 +--- .../Visual/Mods/TestSceneModFailCondition.cs | 4 +--- .../TestSceneMatchBeatmapDetailArea.cs | 4 +--- .../Multiplayer/TestSceneMatchLeaderboard.cs | 4 +--- .../Multiplayer/TestSceneMultiHeader.cs | 2 -- ...TestSceneMultiplayerGameplayLeaderboard.cs | 4 +--- ...ceneMultiplayerGameplayLeaderboardTeams.cs | 4 +--- .../TestSceneMultiplayerMatchFooter.cs | 4 +--- .../Multiplayer/TestSceneRankRangePill.cs | 4 +--- .../TestSceneStarRatingRangeDisplay.cs | 4 +--- .../TestSceneButtonSystemNavigation.cs | 4 +--- .../Navigation/TestSceneEditDefaultSkin.cs | 4 +--- .../TestSceneStartupBeatmapSetDisplay.cs | 4 +--- .../Navigation/TestSceneStartupRuleset.cs | 2 -- .../Navigation/TestSettingsMigration.cs | 4 +--- .../Online/TestSceneBeatmapAvailability.cs | 2 -- .../Online/TestSceneBeatmapSetOverlay.cs | 4 +--- .../TestSceneChangelogSupporterPromo.cs | 4 +--- .../Visual/Online/TestSceneCommentsHeader.cs | 2 -- .../Online/TestSceneDashboardOverlay.cs | 2 -- .../Online/TestSceneExternalLinkButton.cs | 2 -- .../Visual/Online/TestSceneGraph.cs | 2 -- .../Visual/Online/TestSceneHomeNewsPanel.cs | 2 -- .../TestSceneLeaderboardScopeSelector.cs | 2 -- .../Visual/Online/TestSceneNewsCard.cs | 2 -- .../TestSceneOnlineBeatmapListingOverlay.cs | 2 -- .../TestSceneOnlineBeatmapSetOverlay.cs | 2 -- .../Online/TestSceneOnlineViewContainer.cs | 2 -- .../Online/TestSceneRankingsCountryFilter.cs | 2 -- .../Visual/Online/TestSceneRankingsHeader.cs | 2 -- .../Online/TestSceneSpotlightsLayout.cs | 2 -- .../Online/TestSceneTotalCommentsCounter.cs | 2 -- .../Visual/Online/TestSceneWikiMainPage.cs | 4 +--- .../TestScenePlaylistsParticipantsList.cs | 4 +--- .../Playlists/TestScenePlaylistsScreen.cs | 2 -- .../Visual/Ranking/TestSceneAccuracyCircle.cs | 4 +--- .../TestSceneContractedPanelMiddleContent.cs | 4 +--- .../TestSceneExpandedPanelTopContent.cs | 4 +--- .../Settings/TestSceneDirectorySelector.cs | 4 +--- .../Visual/Settings/TestSceneFileSelector.cs | 6 ++--- .../Settings/TestSceneMigrationScreens.cs | 4 +--- .../Settings/TestSceneSettingsSource.cs | 2 -- .../TestSceneBeatmapOptionsOverlay.cs | 2 -- .../TestSceneUserTopScoreContainer.cs | 2 -- .../UserInterface/TestSceneBackButton.cs | 4 +--- .../TestSceneBeatmapListingSortTabControl.cs | 2 -- .../TestSceneBeatmapSearchFilter.cs | 2 -- .../TestSceneBreadcrumbControl.cs | 2 -- .../UserInterface/TestSceneColourPicker.cs | 4 +--- .../TestSceneCommentRepliesButton.cs | 2 -- .../UserInterface/TestSceneContextMenu.cs | 2 -- .../Visual/UserInterface/TestSceneCursors.cs | 2 -- .../TestSceneDashboardBeatmapListing.cs | 2 -- .../UserInterface/TestSceneDrawableDate.cs | 4 +--- .../UserInterface/TestSceneEditorSidebar.cs | 4 +--- .../UserInterface/TestSceneExpandingBar.cs | 2 -- .../TestSceneFirstRunScreenBehaviour.cs | 4 +--- .../TestSceneFirstRunScreenBundledBeatmaps.cs | 4 +--- ...TestSceneFirstRunScreenImportFromStable.cs | 4 +--- .../TestSceneFirstRunScreenUIScale.cs | 4 +--- .../TestSceneFooterButtonMods.cs | 4 +--- .../TestSceneHoldToConfirmOverlay.cs | 2 -- .../UserInterface/TestSceneIconButton.cs | 4 +--- .../TestSceneLabelledDropdown.cs | 4 +--- .../TestSceneLabelledSliderBar.cs | 4 +--- .../TestSceneLabelledSwitchButton.cs | 4 +--- .../UserInterface/TestSceneLabelledTextBox.cs | 2 -- .../UserInterface/TestSceneLoadingSpinner.cs | 2 -- .../UserInterface/TestSceneLogoAnimation.cs | 4 +--- .../UserInterface/TestSceneModDisplay.cs | 4 +--- .../UserInterface/TestSceneModSwitchSmall.cs | 4 +--- .../UserInterface/TestSceneModSwitchTiny.cs | 4 +--- .../UserInterface/TestSceneOnScreenDisplay.cs | 2 -- .../UserInterface/TestSceneOsuDropdown.cs | 4 +--- .../Visual/UserInterface/TestSceneOsuLogo.cs | 4 +--- .../UserInterface/TestSceneOsuPopover.cs | 4 +--- .../UserInterface/TestSceneOsuTextBox.cs | 4 +--- .../UserInterface/TestSceneOverlayHeader.cs | 2 -- .../TestSceneOverlayHeaderBackground.cs | 2 -- .../TestSceneOverlayRulesetSelector.cs | 2 -- .../UserInterface/TestScenePageSelector.cs | 2 -- .../TestSceneParallaxContainer.cs | 2 -- .../TestSceneRankingsSortTabControl.cs | 2 -- .../UserInterface/TestSceneRoundedButton.cs | 4 +--- .../TestSceneScreenBreadcrumbControl.cs | 2 -- .../TestSceneSettingsCheckbox.cs | 4 +--- .../TestSceneShearedOverlayHeader.cs | 4 +--- .../TestSceneShearedSearchTextBox.cs | 4 +--- .../UserInterface/TestSceneTabControl.cs | 2 -- .../UserInterface/TestSceneToggleMenuItem.cs | 4 +--- .../UserInterface/TestSceneTwoLayerButton.cs | 2 -- .../UserInterface/TestSceneUserListToolbar.cs | 2 -- .../UserInterface/TestSceneVolumePieces.cs | 2 -- .../UserInterface/TestSceneWaveContainer.cs | 2 -- .../UserInterface/ThemeComparisonTestScene.cs | 4 +--- .../TestSceneDrawableTournamentMatch.cs | 4 +--- .../TestSceneDrawableTournamentTeam.cs | 4 +--- .../Components/TestSceneMatchHeader.cs | 4 +--- .../Components/TestSceneMatchScoreDisplay.cs | 4 +--- .../Components/TestSceneRoundDisplay.cs | 4 +--- .../TestSceneTournamentBeatmapPanel.cs | 6 ++--- .../TestSceneTournamentMatchChatDisplay.cs | 4 +--- .../NonVisual/CustomTourneyDirectoryTest.cs | 4 +--- .../NonVisual/LadderInfoSerialisationTest.cs | 4 +--- .../Screens/TestSceneDrawingsScreen.cs | 4 +--- .../Screens/TestSceneLadderEditorScreen.cs | 4 +--- .../Screens/TestSceneLadderScreen.cs | 4 +--- .../Screens/TestSceneRoundEditorScreen.cs | 4 +--- .../Screens/TestSceneSeedingEditorScreen.cs | 4 +--- .../Screens/TestSceneSeedingScreen.cs | 4 +--- .../Screens/TestSceneSetupScreen.cs | 4 +--- .../Screens/TestSceneShowcaseScreen.cs | 4 +--- .../TestSceneStablePathSelectScreen.cs | 4 +--- .../Screens/TestSceneTeamEditorScreen.cs | 4 +--- .../Screens/TestSceneTeamIntroScreen.cs | 4 +--- .../Screens/TestSceneTeamWinScreen.cs | 4 +--- .../TestSceneTournamentSceneManager.cs | 4 +--- .../TournamentTestBrowser.cs | 4 +--- .../TournamentTestRunner.cs | 4 +--- .../Components/ControlPanel.cs | 2 -- osu.Game.Tournament/Components/DateTextBox.cs | 4 +--- .../Components/DrawableTeamHeader.cs | 4 +--- .../Components/DrawableTeamTitleWithHeader.cs | 4 +--- .../Components/DrawableTeamWithPlayers.cs | 4 +--- .../DrawableTournamentHeaderLogo.cs | 4 +--- .../DrawableTournamentHeaderText.cs | 4 +--- .../Components/IPCErrorDialog.cs | 4 +--- .../Components/RoundDisplay.cs | 4 +--- .../Components/TournamentModIcon.cs | 6 ++--- .../TournamentSpriteTextWithBackground.cs | 4 +--- .../Configuration/TournamentConfigManager.cs | 4 +--- .../IO/TournamentVideoResourceStore.cs | 4 +--- osu.Game.Tournament/IPC/MatchIPCInfo.cs | 4 +--- osu.Game.Tournament/Models/BeatmapChoice.cs | 4 +--- .../Models/LadderEditorInfo.cs | 4 +--- osu.Game.Tournament/Models/LadderInfo.cs | 4 +--- osu.Game.Tournament/Models/SeedingResult.cs | 4 +--- .../Models/TournamentProgression.cs | 4 +--- osu.Game.Tournament/Models/TournamentRound.cs | 4 +--- .../Properties/AssemblyInfo.cs | 2 -- .../Screens/BeatmapInfoScreen.cs | 4 +--- .../Components/DrawingsConfigManager.cs | 2 -- .../Screens/Drawings/Components/Group.cs | 2 -- .../Drawings/Components/GroupContainer.cs | 2 -- .../Screens/Drawings/Components/GroupTeam.cs | 4 +--- .../Screens/Drawings/Components/ITeamList.cs | 2 -- .../Screens/Editors/IModelBacked.cs | 4 +--- .../Screens/Editors/RoundEditorScreen.cs | 8 +++---- .../Screens/Editors/SeedingEditorScreen.cs | 6 ++--- .../Screens/Editors/TeamEditorScreen.cs | 14 +++++------- .../Gameplay/Components/MatchRoundDisplay.cs | 4 +--- .../Gameplay/Components/TeamDisplay.cs | 4 +--- .../Screens/Gameplay/Components/TeamScore.cs | 4 +--- .../Ladder/Components/ProgressionPath.cs | 4 +--- .../Ladder/Components/SettingsTeamDropdown.cs | 4 +--- .../Screens/Ladder/LadderDragContainer.cs | 4 +--- .../Screens/Showcase/ShowcaseScreen.cs | 4 +--- .../Screens/Showcase/TournamentLogo.cs | 4 +--- .../Screens/TournamentScreen.cs | 6 ++--- osu.Game.Tournament/TournamentSpriteText.cs | 4 +--- osu.Game.Tournament/TourneyButton.cs | 4 +--- osu.Game.Tournament/WarningBox.cs | 4 +--- osu.Game/Beatmaps/APIFailTimes.cs | 2 -- .../Beatmaps/BeatmapMetadataInfoExtensions.cs | 4 +--- osu.Game/Beatmaps/BeatmapOnlineStatus.cs | 2 -- osu.Game/Beatmaps/BeatmapSetHypeStatus.cs | 4 +--- .../Beatmaps/BeatmapSetNominationStatus.cs | 4 +--- .../Beatmaps/BeatmapSetOnlineAvailability.cs | 4 +--- osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs | 2 -- osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs | 4 +--- osu.Game/Beatmaps/BeatmapStatisticIcon.cs | 4 +--- osu.Game/Beatmaps/CountdownType.cs | 4 +--- .../Drawables/BeatmapBackgroundSprite.cs | 2 -- .../Cards/BeatmapCardDifficultyList.cs | 4 +--- .../Cards/BeatmapCardDownloadProgressBar.cs | 8 +++---- .../Drawables/Cards/BeatmapCardThumbnail.cs | 4 +--- .../Cards/BeatmapSetFavouriteState.cs | 4 +--- .../Cards/Buttons/BeatmapCardIconButton.cs | 4 +--- .../Cards/ExpandedContentScrollContainer.cs | 4 +--- osu.Game/Beatmaps/Drawables/Cards/IconPill.cs | 2 -- .../Cards/Statistics/FavouritesStatistic.cs | 4 +--- .../Cards/Statistics/PlayCountStatistic.cs | 4 +--- .../Drawables/Cards/StoryboardIconPill.cs | 4 +--- .../Beatmaps/Drawables/Cards/VideoIconPill.cs | 4 +--- .../Drawables/DifficultySpectrumDisplay.cs | 4 +--- .../Beatmaps/Drawables/DownloadProgressBar.cs | 4 +--- .../Beatmaps/Drawables/StarRatingDisplay.cs | 10 ++++----- .../UpdateableBeatmapBackgroundSprite.cs | 6 ++--- osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs | 4 +--- .../Beatmaps/Formats/IHasCustomColours.cs | 4 +--- .../Beatmaps/Formats/JsonBeatmapDecoder.cs | 2 -- ...egacyDifficultyCalculatorBeatmapDecoder.cs | 4 +--- osu.Game/Beatmaps/Formats/Parsing.cs | 4 +--- osu.Game/Beatmaps/IBeatmap.cs | 4 +--- osu.Game/Beatmaps/IBeatmapConverter.cs | 2 -- osu.Game/Beatmaps/IBeatmapProcessor.cs | 2 -- osu.Game/Beatmaps/IBeatmapResourceProvider.cs | 4 +--- .../Beatmaps/Legacy/LegacyControlPointInfo.cs | 4 +--- osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs | 4 +--- osu.Game/Beatmaps/Legacy/LegacyEventType.cs | 4 +--- .../Beatmaps/Legacy/LegacyHitObjectType.cs | 4 +--- .../Beatmaps/Legacy/LegacyHitSoundType.cs | 4 +--- osu.Game/Beatmaps/Legacy/LegacyMods.cs | 2 -- osu.Game/Beatmaps/Legacy/LegacySampleBank.cs | 4 +--- osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs | 4 +--- osu.Game/Beatmaps/Timing/BreakPeriod.cs | 2 -- osu.Game/Configuration/BackgroundSource.cs | 4 +--- .../DevelopmentOsuConfigManager.cs | 4 +--- .../Configuration/DiscordRichPresenceMode.cs | 4 +--- osu.Game/Configuration/HUDVisibilityMode.cs | 4 +--- .../Configuration/InMemoryConfigManager.cs | 2 -- osu.Game/Configuration/OsuConfigManager.cs | 4 +--- .../Configuration/RandomSelectAlgorithm.cs | 2 -- osu.Game/Configuration/ScalingMode.cs | 4 +--- osu.Game/Configuration/ScreenshotFormat.cs | 2 -- .../ScrollVisualisationMethod.cs | 2 -- .../Configuration/SeasonalBackgroundMode.cs | 4 +--- osu.Game/Configuration/SettingsStore.cs | 2 -- .../Configuration/StorageConfigManager.cs | 4 +--- osu.Game/Database/ICanAcceptFiles.cs | 2 -- osu.Game/Database/IHasFiles.cs | 2 -- osu.Game/Database/IHasGuidPrimaryKey.cs | 4 +--- osu.Game/Database/IHasNamedFiles.cs | 4 +--- osu.Game/Database/IHasPrimaryKey.cs | 2 -- osu.Game/Database/IModelFileManager.cs | 2 -- osu.Game/Database/IModelManager.cs | 4 +--- osu.Game/Database/INamedFileInfo.cs | 2 -- osu.Game/Database/IPostNotifications.cs | 4 +--- .../Graphics/Backgrounds/BeatmapBackground.cs | 4 +--- .../Graphics/Backgrounds/SkinBackground.cs | 4 +--- .../Containers/ConstrainedIconContainer.cs | 2 -- .../Containers/ExpandingButtonContainer.cs | 4 +--- osu.Game/Graphics/Containers/IExpandable.cs | 4 +--- .../Containers/IExpandingContainer.cs | 4 +--- .../Markdown/OsuMarkdownContainer.cs | 4 +--- .../Markdown/OsuMarkdownFencedCodeBlock.cs | 4 +--- .../Containers/Markdown/OsuMarkdownHeading.cs | 4 +--- .../Containers/Markdown/OsuMarkdownImage.cs | 4 +--- .../Markdown/OsuMarkdownOrderedListItem.cs | 4 +--- .../Markdown/OsuMarkdownQuoteBlock.cs | 4 +--- .../Markdown/OsuMarkdownSeparator.cs | 4 +--- .../Containers/Markdown/OsuMarkdownTable.cs | 4 +--- .../Markdown/OsuMarkdownTableCell.cs | 4 +--- .../Markdown/OsuMarkdownUnorderedListItem.cs | 4 +--- .../Graphics/Containers/OsuHoverContainer.cs | 2 -- .../ReverseChildIDFillFlowContainer.cs | 4 +--- .../Graphics/Containers/ShakeContainer.cs | 2 -- .../Containers/UserTrackingScrollContainer.cs | 4 +--- osu.Game/Graphics/Containers/WaveContainer.cs | 2 -- .../Cursor/OsuContextMenuContainer.cs | 2 -- .../Graphics/Cursor/OsuTooltipContainer.cs | 2 -- osu.Game/Graphics/DateTooltip.cs | 2 -- osu.Game/Graphics/DrawableDate.cs | 2 -- osu.Game/Graphics/ErrorTextFlowContainer.cs | 4 +--- osu.Game/Graphics/IHasAccentColour.cs | 2 -- .../Vertices/PositionAndColourVertex.cs | 4 +--- osu.Game/Graphics/OsuColour.cs | 2 -- osu.Game/Graphics/OsuIcon.cs | 4 +--- osu.Game/Graphics/ParticleExplosion.cs | 4 +--- .../Graphics/Sprites/GlowingSpriteText.cs | 2 -- osu.Game/Graphics/Sprites/OsuSpriteText.cs | 2 -- osu.Game/Graphics/UserInterface/Bar.cs | 2 -- osu.Game/Graphics/UserInterface/BarGraph.cs | 4 +--- .../UserInterface/BasicSearchTextBox.cs | 4 +--- .../CommaSeparatedScoreCounter.cs | 4 +--- .../UserInterface/DangerousRoundedButton.cs | 4 +--- .../Graphics/UserInterface/DialogButton.cs | 2 -- .../UserInterface/DrawableStatefulMenuItem.cs | 4 +--- .../UserInterface/ExpandableSlider.cs | 8 +++---- .../Graphics/UserInterface/ExpandingBar.cs | 2 -- .../Graphics/UserInterface/HoverSampleSet.cs | 4 +--- osu.Game/Graphics/UserInterface/IconButton.cs | 2 -- .../Graphics/UserInterface/LoadingButton.cs | 2 -- .../Graphics/UserInterface/LoadingSpinner.cs | 2 -- .../UserInterface/OsuAnimatedButton.cs | 4 +--- .../Graphics/UserInterface/OsuContextMenu.cs | 4 +--- .../Graphics/UserInterface/OsuEnumDropdown.cs | 2 -- .../Graphics/UserInterface/OsuNumberBox.cs | 4 +--- .../UserInterface/OsuPasswordTextBox.cs | 4 +--- .../Graphics/UserInterface/OsuTabDropdown.cs | 2 -- .../PageSelector/PageEllipsis.cs | 4 +--- .../PageSelector/PageSelector.cs | 2 -- .../UserInterface/PercentageCounter.cs | 2 -- .../Graphics/UserInterface/SearchTextBox.cs | 2 -- .../UserInterface/SeekLimitedSearchTextBox.cs | 2 -- .../UserInterface/ShearedSearchTextBox.cs | 4 +--- .../UserInterface/SlimEnumDropdown.cs | 2 -- .../Graphics/UserInterface/StarCounter.cs | 2 -- osu.Game/Graphics/UserInterface/TimeSlider.cs | 4 +--- .../Graphics/UserInterface/TwoLayerButton.cs | 2 -- .../UserInterfaceV2/LabelledColourPalette.cs | 4 +--- .../UserInterfaceV2/LabelledComponent.cs | 4 +--- .../UserInterfaceV2/LabelledDropdown.cs | 4 +--- .../UserInterfaceV2/LabelledEnumDropdown.cs | 4 +--- .../UserInterfaceV2/LabelledNumberBox.cs | 4 +--- .../UserInterfaceV2/LabelledSliderBar.cs | 4 +--- .../UserInterfaceV2/LabelledSwitchButton.cs | 4 +--- .../UserInterfaceV2/OsuColourPicker.cs | 4 +--- .../OsuDirectorySelectorParentDirectory.cs | 4 +--- .../UserInterfaceV2/OsuHSVColourPicker.cs | 4 +--- .../UserInterfaceV2/OsuHexColourPicker.cs | 4 +--- .../Graphics/UserInterfaceV2/OsuPopover.cs | 4 +--- osu.Game/IO/Archives/LegacyByteArrayReader.cs | 4 +--- .../Archives/LegacyDirectoryArchiveReader.cs | 2 -- .../IO/Archives/LegacyFileArchiveReader.cs | 4 +--- .../FileAbstraction/StreamFileAbstraction.cs | 4 +--- osu.Game/IO/Legacy/ILegacySerializable.cs | 4 +--- osu.Game/IO/OsuStorage.cs | 4 +--- .../SnakeCaseStringEnumConverter.cs | 4 +--- .../SnakeCaseKeyContractResolver.cs | 2 -- osu.Game/Input/IdleTracker.cs | 4 +--- osu.Game/Input/OsuConfineMouseMode.cs | 4 +--- osu.Game/Input/OsuUserInputManager.cs | 4 +--- osu.Game/Online/API/APIException.cs | 4 +--- osu.Game/Online/API/APIMessagesRequest.cs | 2 -- .../API/ModSettingsDictionaryFormatter.cs | 4 +--- osu.Game/Online/API/OsuJsonWebRequest.cs | 4 +--- osu.Game/Online/API/OsuWebRequest.cs | 4 +--- .../Online/API/Requests/CommentVoteRequest.cs | 2 -- .../API/Requests/CreateChannelRequest.cs | 4 +--- .../CreateNewPrivateMessageRequest.cs | 2 -- osu.Game/Online/API/Requests/Cursor.cs | 4 +--- .../API/Requests/DownloadBeatmapSetRequest.cs | 2 -- .../API/Requests/DownloadReplayRequest.cs | 2 -- .../API/Requests/GetBeatmapSetRequest.cs | 2 -- .../Online/API/Requests/GetBeatmapsRequest.cs | 4 +--- .../API/Requests/GetChangelogBuildRequest.cs | 2 -- .../API/Requests/GetChangelogRequest.cs | 2 -- .../Online/API/Requests/GetCommentsRequest.cs | 2 -- .../API/Requests/GetCountryRankingsRequest.cs | 2 -- .../Online/API/Requests/GetFriendsRequest.cs | 2 -- .../Online/API/Requests/GetMessagesRequest.cs | 2 -- .../Online/API/Requests/GetRankingsRequest.cs | 2 -- .../Requests/GetSeasonalBackgroundsRequest.cs | 2 -- .../Requests/GetSpotlightRankingsRequest.cs | 2 -- .../Online/API/Requests/GetTopUsersRequest.cs | 2 -- .../API/Requests/GetUserBeatmapsRequest.cs | 2 -- .../Requests/GetUserKudosuHistoryRequest.cs | 2 -- .../GetUserMostPlayedBeatmapsRequest.cs | 2 -- .../API/Requests/GetUserRankingsRequest.cs | 2 -- .../GetUserRecentActivitiesRequest.cs | 2 -- .../Online/API/Requests/GetWikiRequest.cs | 4 +--- .../Online/API/Requests/JoinChannelRequest.cs | 2 -- .../API/Requests/LeaveChannelRequest.cs | 2 -- .../API/Requests/ListChannelsRequest.cs | 2 -- .../API/Requests/MarkChannelAsReadRequest.cs | 2 -- .../API/Requests/PaginatedAPIRequest.cs | 4 +--- .../API/Requests/PaginationParameters.cs | 4 +--- .../Requests/PostBeatmapFavouriteRequest.cs | 2 -- .../Online/API/Requests/PostMessageRequest.cs | 2 -- .../API/Requests/Responses/APIPlayStyle.cs | 4 +--- .../Requests/Responses/APIUserAchievement.cs | 4 +--- .../Requests/Responses/APIUserHistoryCount.cs | 4 +--- osu.Game/Online/Chat/DrawableLinkCompiler.cs | 6 ++--- osu.Game/Online/Chat/ErrorMessage.cs | 4 +--- .../DevelopmentEndpointConfiguration.cs | 4 +--- osu.Game/Online/Leaderboards/DrawableRank.cs | 2 -- .../Multiplayer/IMultiplayerLoungeServer.cs | 4 +--- .../Multiplayer/IMultiplayerRoomServer.cs | 4 +--- .../Multiplayer/InvalidPasswordException.cs | 4 +--- .../InvalidStateChangeException.cs | 4 +--- .../Multiplayer/InvalidStateException.cs | 4 +--- .../Online/Multiplayer/MatchUserRequest.cs | 4 +--- .../Online/Multiplayer/NotHostException.cs | 4 +--- .../Multiplayer/NotJoinedRoomException.cs | 4 +--- osu.Game/Online/Multiplayer/QueueMode.cs | 4 +--- .../Online/Placeholders/LoginPlaceholder.cs | 6 ++--- .../Online/Placeholders/MessagePlaceholder.cs | 2 -- osu.Game/Online/Rooms/APIScoreToken.cs | 4 +--- osu.Game/Online/Rooms/CreateRoomRequest.cs | 4 +--- .../Online/Rooms/CreateRoomScoreRequest.cs | 4 +--- .../Online/Rooms/GetRoomLeaderboardRequest.cs | 4 +--- osu.Game/Online/Rooms/GetRoomRequest.cs | 4 +--- osu.Game/Online/Rooms/GetRoomsRequest.cs | 4 +--- osu.Game/Online/Rooms/IndexScoresParams.cs | 4 +--- osu.Game/Online/Rooms/ItemAttemptsCount.cs | 4 +--- osu.Game/Online/Rooms/JoinRoomRequest.cs | 4 +--- osu.Game/Online/Rooms/MatchType.cs | 4 +--- osu.Game/Online/Rooms/MultiplayerScores.cs | 4 +--- osu.Game/Online/Rooms/PartRoomRequest.cs | 4 +--- osu.Game/Online/Rooms/RoomAvailability.cs | 2 -- osu.Game/Online/Rooms/RoomCategory.cs | 4 +--- .../Rooms/RoomStatuses/RoomStatusEnded.cs | 4 +--- .../Rooms/RoomStatuses/RoomStatusOpen.cs | 4 +--- .../Rooms/RoomStatuses/RoomStatusPlaying.cs | 4 +--- .../Rooms/ShowPlaylistUserScoreRequest.cs | 4 +--- .../Online/Rooms/SubmitRoomScoreRequest.cs | 4 +--- osu.Game/Online/Rooms/SubmitScoreRequest.cs | 4 +--- osu.Game/Online/SignalRWorkaroundTypes.cs | 4 +--- .../Online/Solo/CreateSoloScoreRequest.cs | 4 +--- .../Online/Solo/SubmitSoloScoreRequest.cs | 4 +--- osu.Game/Online/Spectator/ISpectatorClient.cs | 4 +--- osu.Game/Online/Spectator/ISpectatorServer.cs | 4 +--- osu.Game/OsuGameBase_Importing.cs | 4 +--- .../AccountCreationBackground.cs | 4 +--- .../AccountCreation/AccountCreationScreen.cs | 4 +--- .../BeatmapListingSortTabControl.cs | 2 -- .../BeatmapListing/BeatmapSearchFilterRow.cs | 2 -- .../BeatmapSearchRulesetFilterRow.cs | 2 -- .../BeatmapSearchScoreFilterRow.cs | 2 -- .../Overlays/BeatmapListing/SearchCategory.cs | 4 +--- .../Overlays/BeatmapListing/SearchExplicit.cs | 4 +--- .../Overlays/BeatmapListing/SearchExtra.cs | 2 -- .../Overlays/BeatmapListing/SearchGeneral.cs | 4 +--- .../Overlays/BeatmapListing/SearchGenre.cs | 4 +--- .../Overlays/BeatmapListing/SearchLanguage.cs | 4 +--- .../Overlays/BeatmapListing/SearchPlayed.cs | 2 -- .../Overlays/BeatmapListing/SortCriteria.cs | 4 +--- .../BeatmapSet/BeatmapRulesetSelector.cs | 2 -- .../BeatmapSet/BeatmapSetLayoutSection.cs | 2 -- .../BeatmapSet/Buttons/PreviewButton.cs | 2 -- osu.Game/Overlays/BeatmapSet/Info.cs | 2 -- .../BeatmapSet/LeaderboardScopeSelector.cs | 2 -- osu.Game/Overlays/BeatmapSet/MetadataType.cs | 4 +--- .../BeatmapSet/Scores/NoScoresPlaceholder.cs | 2 -- .../Scores/NotSupporterPlaceholder.cs | 2 -- .../Scores/ScoreTableRowBackground.cs | 4 +--- .../BeatmapSet/Scores/ScoreboardTime.cs | 4 +--- .../BeatmapSet/Scores/TopScoreUserSection.cs | 4 +--- .../BreadcrumbControlOverlayHeader.cs | 2 -- osu.Game/Overlays/Changelog/ChangelogEntry.cs | 8 +++---- .../Overlays/Changelog/ChangelogListing.cs | 4 +--- .../Changelog/ChangelogSupporterPromo.cs | 4 +--- .../Changelog/ChangelogUpdateStreamControl.cs | 2 -- .../Overlays/Chat/ChannelScrollContainer.cs | 4 +--- .../Comments/Buttons/ChevronButton.cs | 2 -- .../Comments/Buttons/CommentRepliesButton.cs | 4 +--- .../Comments/CommentMarkdownContainer.cs | 4 +--- .../Comments/CommentsShowMoreButton.cs | 2 -- .../Comments/DeletedCommentsCounter.cs | 2 -- osu.Game/Overlays/Comments/HeaderButton.cs | 2 -- .../Dashboard/DashboardOverlayHeader.cs | 2 -- .../Dashboard/Friends/FriendStream.cs | 2 -- .../Friends/FriendsOnlineStatusItem.cs | 2 -- .../Dashboard/Friends/OnlineStatus.cs | 4 +--- .../Dashboard/Friends/UserListToolbar.cs | 2 -- .../Dashboard/Friends/UserSortTabControl.cs | 2 -- .../Dashboard/Home/DashboardBeatmapListing.cs | 2 -- .../Home/DashboardNewBeatmapPanel.cs | 2 -- .../Home/DashboardPopularBeatmapPanel.cs | 2 -- .../Dashboard/Home/DrawableBeatmapList.cs | 2 -- .../Dashboard/Home/DrawableNewBeatmapList.cs | 2 -- .../Home/DrawablePopularBeatmapList.cs | 2 -- osu.Game/Overlays/Dashboard/Home/HomePanel.cs | 2 -- .../Home/News/FeaturedNewsItemPanel.cs | 2 -- .../Dashboard/Home/News/NewsGroupItem.cs | 2 -- .../Dashboard/Home/News/NewsItemGroupPanel.cs | 2 -- .../Dashboard/Home/News/NewsTitleLink.cs | 2 -- osu.Game/Overlays/DashboardOverlay.cs | 2 -- osu.Game/Overlays/Dialog/PopupDialog.cs | 2 -- osu.Game/Overlays/Dialog/PopupDialogButton.cs | 2 -- .../Dialog/PopupDialogCancelButton.cs | 2 -- .../Overlays/Dialog/PopupDialogOkButton.cs | 2 -- osu.Game/Overlays/FullscreenOverlay.cs | 6 ++--- osu.Game/Overlays/INamedOverlayComponent.cs | 4 +--- osu.Game/Overlays/INotificationOverlay.cs | 4 +--- osu.Game/Overlays/IOverlayManager.cs | 4 +--- osu.Game/Overlays/Login/UserDropdown.cs | 4 +--- .../Overlays/Mods/DeselectAllModsButton.cs | 4 +--- .../Mods/DifficultyMultiplierDisplay.cs | 4 +--- .../Mods/IncompatibilityDisplayingModPanel.cs | 6 ++--- .../Mods/IncompatibilityDisplayingTooltip.cs | 6 ++--- osu.Game/Overlays/Mods/ModSettingsArea.cs | 6 ++--- .../Overlays/Music/MusicKeyBindingHandler.cs | 16 ++++++-------- .../Music/NowPlayingCollectionDropdown.cs | 2 -- osu.Game/Overlays/News/NewsPostBackground.cs | 2 -- .../Notifications/SimpleErrorNotification.cs | 4 +--- osu.Game/Overlays/OSD/Toast.cs | 2 -- osu.Game/Overlays/OnlineOverlay.cs | 2 -- osu.Game/Overlays/OverlayColourProvider.cs | 2 -- osu.Game/Overlays/OverlayHeader.cs | 4 +--- osu.Game/Overlays/OverlayHeaderBackground.cs | 2 -- osu.Game/Overlays/OverlayRulesetSelector.cs | 2 -- osu.Game/Overlays/OverlayRulesetTabItem.cs | 4 +--- osu.Game/Overlays/OverlaySidebar.cs | 4 +--- osu.Game/Overlays/Rankings/CountryFilter.cs | 4 +--- osu.Game/Overlays/Rankings/CountryPill.cs | 2 -- osu.Game/Overlays/Rankings/RankingsScope.cs | 4 +--- .../Rankings/RankingsSortTabControl.cs | 2 -- .../Rankings/Tables/CountriesTable.cs | 8 +++---- .../Rankings/Tables/PerformanceTable.cs | 2 -- .../Overlays/Rankings/Tables/ScoresTable.cs | 2 -- .../Rankings/Tables/TableRowBackground.cs | 4 +--- .../Settings/DangerousSettingsButton.cs | 2 -- osu.Game/Overlays/Settings/ISettingsItem.cs | 4 +--- osu.Game/Overlays/Settings/OutlinedTextBox.cs | 4 +--- .../Settings/Sections/Audio/OffsetSettings.cs | 2 -- .../Settings/Sections/Audio/VolumeSettings.cs | 2 -- .../Settings/Sections/AudioSection.cs | 2 -- .../Settings/Sections/DebugSection.cs | 2 -- .../Sections/DebugSettings/GeneralSettings.cs | 2 -- .../Sections/DebugSettings/MemorySettings.cs | 2 -- .../Sections/Gameplay/AudioSettings.cs | 4 +--- .../Sections/Gameplay/BackgroundSettings.cs | 4 +--- .../Sections/Gameplay/BeatmapSettings.cs | 4 +--- .../Sections/Gameplay/GeneralSettings.cs | 4 +--- .../Settings/Sections/Gameplay/HUDSettings.cs | 2 -- .../Sections/Gameplay/InputSettings.cs | 4 +--- .../Sections/Gameplay/ModsSettings.cs | 2 -- .../Settings/Sections/GameplaySection.cs | 2 -- .../Sections/Graphics/ScreenshotSettings.cs | 2 -- .../Settings/Sections/GraphicsSection.cs | 2 -- .../Sections/Input/BindingSettings.cs | 2 -- .../Sections/Input/KeyBindingPanel.cs | 4 +--- .../Sections/Input/RulesetBindingsSection.cs | 4 +--- .../Settings/Sections/InputSection.cs | 2 -- .../StableDirectoryLocationDialog.cs | 4 +--- .../StableDirectorySelectScreen.cs | 2 -- .../Online/AlertsAndPrivacySettings.cs | 2 -- .../Sections/Online/IntegrationSettings.cs | 4 +--- .../Settings/Sections/Online/WebSettings.cs | 2 -- .../Settings/Sections/OnlineSection.cs | 2 -- .../Overlays/Settings/Sections/SizeSlider.cs | 4 +--- .../Sections/UserInterface/GeneralSettings.cs | 2 -- .../UserInterface/SongSelectSettings.cs | 2 -- .../Settings/Sections/UserInterfaceSection.cs | 4 +--- .../Overlays/Settings/SettingsCheckbox.cs | 4 +--- .../Overlays/Settings/SettingsEnumDropdown.cs | 2 -- osu.Game/Overlays/Settings/SettingsFooter.cs | 4 +--- osu.Game/Overlays/Settings/SettingsHeader.cs | 2 -- .../Overlays/Settings/SettingsNumberBox.cs | 4 +--- osu.Game/Overlays/Settings/SettingsSidebar.cs | 2 -- osu.Game/Overlays/Settings/SettingsSlider.cs | 2 -- .../Overlays/Settings/SettingsSubsection.cs | 2 -- osu.Game/Overlays/Settings/SettingsTextBox.cs | 2 -- osu.Game/Overlays/Settings/SidebarButton.cs | 6 ++--- osu.Game/Overlays/TabControlOverlayHeader.cs | 2 -- osu.Game/Overlays/Toolbar/ClockDisplay.cs | 4 +--- .../Toolbar/ToolbarBeatmapListingButton.cs | 4 +--- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 8 +++---- .../Toolbar/ToolbarChangelogButton.cs | 4 +--- .../Overlays/Toolbar/ToolbarChatButton.cs | 4 +--- .../Overlays/Toolbar/ToolbarHomeButton.cs | 2 -- .../Overlays/Toolbar/ToolbarNewsButton.cs | 2 -- .../Toolbar/ToolbarNotificationButton.cs | 2 -- .../Overlays/Toolbar/ToolbarRankingsButton.cs | 2 -- .../Toolbar/ToolbarRulesetTabButton.cs | 2 -- .../Overlays/Toolbar/ToolbarSettingsButton.cs | 2 -- .../Overlays/Toolbar/ToolbarSocialButton.cs | 4 +--- .../Overlays/Toolbar/ToolbarWikiButton.cs | 4 +--- osu.Game/Overlays/VersionManager.cs | 2 -- osu.Game/Overlays/Volume/MuteButton.cs | 2 -- osu.Game/Overlays/WaveOverlayContainer.cs | 2 -- .../Wiki/Markdown/WikiMarkdownContainer.cs | 4 +--- .../Wiki/Markdown/WikiMarkdownImage.cs | 4 +--- .../Wiki/Markdown/WikiMarkdownImageBlock.cs | 6 ++--- .../Wiki/Markdown/WikiNoticeContainer.cs | 6 ++--- .../Performance/HighPerformanceSession.cs | 4 +--- osu.Game/Properties/AssemblyInfo.cs | 2 -- .../Difficulty/PerformanceAttributes.cs | 2 -- .../Difficulty/PerformanceCalculator.cs | 4 +--- .../Difficulty/PerformanceDisplayAttribute.cs | 4 +--- osu.Game/Rulesets/Difficulty/Skills/Skill.cs | 4 +--- .../Difficulty/Skills/StrainDecaySkill.cs | 4 +--- .../Rulesets/Difficulty/Skills/StrainSkill.cs | 4 +--- osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 4 +--- .../Edit/DrawableEditorRulesetWrapper.cs | 10 ++++----- osu.Game/Rulesets/Edit/EditorToolboxGroup.cs | 4 +--- .../Edit/ExpandingToolboxContainer.cs | 4 +--- osu.Game/Rulesets/Edit/IBeatmapVerifier.cs | 4 +--- .../Rulesets/Edit/IDistanceSnapProvider.cs | 4 +--- .../Rulesets/Edit/IPositionSnapProvider.cs | 4 +--- osu.Game/Rulesets/Edit/SnapType.cs | 4 +--- .../Rulesets/Judgements/IgnoreJudgement.cs | 4 +--- osu.Game/Rulesets/Judgements/Judgement.cs | 2 -- .../Rulesets/Judgements/JudgementResult.cs | 4 +--- osu.Game/Rulesets/Objects/HitObjectParser.cs | 2 -- .../Objects/Legacy/Catch/ConvertHit.cs | 2 -- .../Objects/Legacy/Catch/ConvertSlider.cs | 2 -- .../Objects/Legacy/Catch/ConvertSpinner.cs | 2 -- .../Objects/Legacy/ConvertHitObject.cs | 4 +--- .../Legacy/Mania/ConvertHitObjectParser.cs | 2 -- .../Objects/Legacy/Mania/ConvertHold.cs | 2 -- .../Objects/Legacy/Mania/ConvertSpinner.cs | 2 -- .../Rulesets/Objects/Legacy/Osu/ConvertHit.cs | 2 -- .../Objects/Legacy/Osu/ConvertSlider.cs | 2 -- .../Objects/Legacy/Osu/ConvertSpinner.cs | 2 -- .../Objects/Legacy/Taiko/ConvertSpinner.cs | 2 -- .../Rulesets/Objects/SliderEventGenerator.cs | 4 +--- osu.Game/Rulesets/Objects/SliderPath.cs | 4 +--- .../Objects/SyntheticHitObjectEntry.cs | 4 +--- .../Objects/Types/IHasDisplayColour.cs | 4 +--- osu.Game/Rulesets/Objects/Types/IHasPath.cs | 4 +--- .../Objects/Types/IHasPathWithRepeats.cs | 2 -- .../Rulesets/Objects/Types/IHasPosition.cs | 2 -- .../Rulesets/Objects/Types/IHasRepeats.cs | 2 -- osu.Game/Rulesets/RulesetLoadException.cs | 4 +--- .../Scoring/AccumulatingHealthProcessor.cs | 4 +--- .../Rulesets/Scoring/HitEventExtensions.cs | 4 +--- osu.Game/Rulesets/Scoring/HitWindows.cs | 2 -- .../Rulesets/UI/GameplayCursorContainer.cs | 4 +--- osu.Game/Rulesets/UI/IHitObjectContainer.cs | 4 +--- .../Rulesets/UI/IPooledHitObjectProvider.cs | 4 +--- osu.Game/Rulesets/UI/JudgementContainer.cs | 2 -- .../UI/PlayfieldAdjustmentContainer.cs | 4 +--- osu.Game/Rulesets/UI/PlayfieldBorder.cs | 2 -- osu.Game/Rulesets/UI/PlayfieldBorderStyle.cs | 4 +--- .../Algorithms/ConstantScrollAlgorithm.cs | 2 -- .../Rulesets/UI/Scrolling/IScrollingInfo.cs | 2 -- .../UI/Scrolling/ScrollingPlayfield.cs | 6 ++--- osu.Game/Scoring/HitResultDisplayStatistic.cs | 4 +--- osu.Game/Scoring/IScoreInfo.cs | 4 +--- .../Scoring/Legacy/ScoreInfoExtensions.cs | 4 +--- osu.Game/Scoring/Score.cs | 4 +--- osu.Game/Scoring/ScoreRank.cs | 2 -- osu.Game/Screens/BackgroundScreenStack.cs | 4 +--- .../Backgrounds/BackgroundScreenBlack.cs | 2 -- .../Backgrounds/BackgroundScreenCustom.cs | 2 -- .../Components/BeatDivisorPresetCollection.cs | 4 +--- .../Components/CircularDistanceSnapGrid.cs | 12 +++++----- .../Components/EditorSelectionHandler.cs | 6 ++--- .../HitObjectOrderedSelectionContainer.cs | 6 ++--- .../Compose/Components/MoveSelectionEvent.cs | 4 +--- .../Components/RectangularPositionSnapGrid.cs | 4 +--- .../Components/Timeline/CentreMarker.cs | 2 -- .../Components/Timeline/TimelineButton.cs | 2 -- .../Timeline/TimelineControlPointDisplay.cs | 4 +--- .../Timeline/TimelineControlPointGroup.cs | 4 +--- .../Components/Timeline/TimelineDragBox.cs | 6 ++--- .../Timeline/TimelineTickDisplay.cs | 22 +++++++++---------- .../Components/Timeline/TimingPointPiece.cs | 4 +--- .../Timeline/ZoomableScrollContainer.cs | 6 ++--- .../Screens/Edit/Compose/IPlacementHandler.cs | 4 +--- osu.Game/Screens/Edit/EditorClipboard.cs | 4 +--- .../Edit/EditorRoundedScreenSettings.cs | 4 +--- osu.Game/Screens/Edit/EditorScreen.cs | 6 ++--- .../Edit/GameplayTest/EditorPlayerLoader.cs | 6 ++--- .../SaveBeforeGameplayTestDialog.cs | 4 +--- .../Screens/Edit/HitAnimationsMenuItem.cs | 4 +--- .../Edit/Verify/InterpretationSection.cs | 4 +--- osu.Game/Screens/Edit/Verify/IssueSettings.cs | 4 +--- .../Screens/Edit/WaveformOpacityMenuItem.cs | 4 +--- osu.Game/Screens/IHandlePresentBeatmap.cs | 4 +--- osu.Game/Screens/IHasSubScreenStack.cs | 4 +--- osu.Game/Screens/IOsuScreen.cs | 4 +--- osu.Game/Screens/Menu/ExitConfirmOverlay.cs | 4 +--- osu.Game/Screens/Menu/SongTicker.cs | 4 +--- osu.Game/Screens/Menu/StorageErrorDialog.cs | 6 ++--- .../BeatmapDetailAreaPlaylistTabItem.cs | 4 +--- .../OnlinePlay/Components/BeatmapTitle.cs | 4 +--- .../Components/DisableableTabControl.cs | 4 +--- .../OnlinePlay/Components/DrawableGameType.cs | 6 ++--- .../OnlinePlay/Components/OverlinedHeader.cs | 4 +--- .../Components/OverlinedPlaylistHeader.cs | 4 +--- .../Components/ParticipantsDisplay.cs | 4 +--- .../OnlinePlay/Components/ReadyButton.cs | 4 +--- .../Components/RoomPollingComponent.cs | 8 +++---- .../OnlinePlay/FooterButtonFreeMods.cs | 4 +--- .../OnlinePlay/IOnlinePlaySubScreen.cs | 4 +--- .../Lounge/Components/EndDateInfo.cs | 4 +--- .../Lounge/Components/MatchTypePill.cs | 4 +--- .../Lounge/Components/PillContainer.cs | 4 +--- .../Lounge/Components/PlaylistCountPill.cs | 4 +--- .../Lounge/Components/QueueModePill.cs | 4 +--- .../Components/RoomSpecialCategoryPill.cs | 6 ++--- .../Lounge/Components/RoomStatusFilter.cs | 4 +--- .../Lounge/Components/RoomStatusPill.cs | 6 ++--- .../Match/Components/CreateRoomButton.cs | 4 +--- .../Match/Components/MatchChatDisplay.cs | 8 +++---- .../OnlinePlay/Match/RoomBackgroundScreen.cs | 4 +--- .../Multiplayer/GameplayMatchScoreDisplay.cs | 4 +--- .../Match/Playlist/MultiplayerHistoryList.cs | 4 +--- .../Playlist/MultiplayerPlaylistTabControl.cs | 2 -- .../OnlinePlay/Multiplayer/Multiplayer.cs | 6 ++--- .../Multiplayer/MultiplayerResultsScreen.cs | 4 +--- .../Participants/ParticipantsListHeader.cs | 6 ++--- .../Spectate/MultiSpectatorPlayerLoader.cs | 4 +--- .../Multiplayer/Spectate/PlayerGrid.cs | 4 +--- .../Multiplayer/Spectate/PlayerGrid_Facade.cs | 4 +--- .../OnlinePlay/OnlinePlaySubScreenStack.cs | 4 +--- .../Playlists/CreatePlaylistsRoomButton.cs | 4 +--- .../Screens/OnlinePlay/Playlists/Playlists.cs | 4 +--- .../PlaylistsRoomSettingsPlaylist.cs | 4 +--- osu.Game/Screens/OsuScreenStack.cs | 4 +--- osu.Game/Screens/Play/Break/BlurredIcon.cs | 2 -- osu.Game/Screens/Play/Break/BreakArrows.cs | 2 -- osu.Game/Screens/Play/Break/BreakInfo.cs | 2 -- osu.Game/Screens/Play/Break/GlowIcon.cs | 2 -- .../Play/Break/RemainingTimeCounter.cs | 2 -- .../Play/HUD/DefaultAccuracyCounter.cs | 4 +--- .../Screens/Play/HUD/DefaultHealthDisplay.cs | 2 -- .../Screens/Play/HUD/DefaultScoreCounter.cs | 4 +--- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 8 +++---- osu.Game/Screens/Play/HUD/ModDisplay.cs | 2 -- osu.Game/Screens/Play/HUD/ModFlowDisplay.cs | 4 +--- .../Screens/Play/HUD/PlayerSettingsOverlay.cs | 2 -- .../Screens/Play/HUD/UnstableRateCounter.cs | 6 ++--- osu.Game/Screens/Play/HotkeyExitOverlay.cs | 2 -- osu.Game/Screens/Play/HotkeyRetryOverlay.cs | 2 -- osu.Game/Screens/Play/ILocalUserPlayInfo.cs | 4 +--- osu.Game/Screens/Play/PlayerConfiguration.cs | 4 +--- .../Play/PlayerSettings/DiscussionSettings.cs | 2 -- .../Play/PlayerSettings/InputSettings.cs | 2 -- .../Play/PlayerSettings/PlaybackSettings.cs | 2 -- .../Play/PlayerSettings/PlayerSliderBar.cs | 4 +--- osu.Game/Screens/Play/ReplayPlayerLoader.cs | 4 +--- .../Screens/Play/SpectatorPlayerLoader.cs | 4 +--- .../Screens/Play/SpectatorResultsScreen.cs | 6 ++--- osu.Game/Screens/Ranking/AspectContainer.cs | 4 +--- .../ContractedPanelMiddleContent.cs | 6 ++--- .../Ranking/Expanded/Accuracy/RankNotch.cs | 4 +--- .../Expanded/Statistics/HitResultStatistic.cs | 4 +--- .../Expanded/Statistics/StatisticCounter.cs | 2 -- osu.Game/Screens/Ranking/RetryButton.cs | 6 ++--- .../Ranking/ScorePanelTrackingContainer.cs | 4 +--- .../Ranking/Statistics/AverageHitError.cs | 4 +--- .../Ranking/Statistics/StatisticContainer.cs | 4 +--- .../Ranking/Statistics/StatisticItem.cs | 4 +--- .../Ranking/Statistics/UnstableRate.cs | 4 +--- .../Select/BeatmapDetailAreaDetailTabItem.cs | 4 +--- .../BeatmapDetailAreaLeaderboardTabItem.cs | 4 +--- .../Select/BeatmapInfoWedgeBackground.cs | 2 -- osu.Game/Screens/Select/Filter/GroupMode.cs | 2 -- osu.Game/Screens/Select/Filter/SortMode.cs | 2 -- .../Screens/Select/FooterButtonOptions.cs | 4 +--- .../Leaderboards/BeatmapLeaderboardScope.cs | 2 -- .../Select/Options/BeatmapOptionsButton.cs | 2 -- .../Spectate/SpectatorGameplayState.cs | 4 +--- osu.Game/Screens/StartupScreen.cs | 4 +--- .../Skinning/LegacyManiaSkinConfiguration.cs | 4 +--- osu.Game/Storyboards/CommandLoop.cs | 2 -- osu.Game/Storyboards/CommandTimelineGroup.cs | 2 -- osu.Game/Storyboards/CommandTrigger.cs | 2 -- .../Drawables/DrawableStoryboardLayer.cs | 2 -- .../Drawables/DrawableStoryboardSample.cs | 6 ++--- .../Drawables/DrawableStoryboardSprite.cs | 4 +--- .../Drawables/DrawablesExtensions.cs | 2 -- osu.Game/Storyboards/Drawables/IFlippable.cs | 2 -- .../Storyboards/Drawables/IVectorScalable.cs | 4 +--- osu.Game/Storyboards/IStoryboardElement.cs | 2 -- .../IStoryboardElementWithDuration.cs | 4 +--- osu.Game/Storyboards/StoryboardExtensions.cs | 4 +--- osu.Game/Storyboards/StoryboardLayer.cs | 2 -- osu.Game/Storyboards/StoryboardSample.cs | 2 -- osu.Game/Storyboards/StoryboardVideo.cs | 2 -- osu.Game/Storyboards/StoryboardVideoLayer.cs | 4 +--- .../Tests/Beatmaps/LegacyModConversionTest.cs | 2 -- osu.Game/Tests/Beatmaps/TestBeatmap.cs | 2 -- osu.Game/Tests/CleanRunHeadlessGameHost.cs | 2 -- osu.Game/Tests/OsuTestBrowser.cs | 2 -- .../Visual/DependencyProvidingContainer.cs | 4 +--- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 4 +--- .../IMultiplayerTestSceneDependencies.cs | 4 +--- .../MultiplayerTestSceneDependencies.cs | 4 +--- .../IOnlinePlayTestSceneDependencies.cs | 4 +--- .../OnlinePlayTestSceneDependencies.cs | 4 +--- osu.Game/Tests/Visual/OsuGridTestScene.cs | 4 +--- .../Visual/OsuManualInputManagerTestScene.cs | 2 -- osu.Game/Tests/Visual/ScreenTestScene.cs | 2 -- osu.Game/Tests/Visual/TestReplayPlayer.cs | 4 +--- osu.Game/Tests/VisualTestRunner.cs | 2 -- osu.Game/Users/CountryStatistics.cs | 2 -- osu.Game/Users/Drawables/DrawableFlag.cs | 2 -- osu.Game/Users/UserActivity.cs | 2 -- osu.Game/Users/UserBrickPanel.cs | 2 -- osu.Game/Users/UserGridPanel.cs | 2 -- osu.Game/Users/UserListPanel.cs | 2 -- osu.Game/Users/UserStatus.cs | 4 +--- osu.Game/Utils/LimitedCapacityQueue.cs | 4 +--- osu.iOS/OsuGameIOS.cs | 2 -- 1072 files changed, 786 insertions(+), 2930 deletions(-) diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs index 3c39a820cc..d77b24722a 100644 --- a/osu.Android/GameplayScreenRotationLocker.cs +++ b/osu.Android/GameplayScreenRotationLocker.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Android.Content.PM; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -16,7 +14,7 @@ namespace osu.Android private Bindable localUserPlaying; [Resolved] - private OsuGameActivity gameActivity { get; set; } + private OsuGameActivity gameActivity { get; set; } = null!; [BackgroundDependencyLoader] private void load(OsuGame game) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index f0a6e4733c..81b218436e 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 0227d2aec2..96f81c209c 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using Android.App; using Microsoft.Maui.Devices; diff --git a/osu.Android/Properties/AssemblyInfo.cs b/osu.Android/Properties/AssemblyInfo.cs index f65b1b239f..1632087fb1 100644 --- a/osu.Android/Properties/AssemblyInfo.cs +++ b/osu.Android/Properties/AssemblyInfo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Android; using Android.App; diff --git a/osu.Game.Rulesets.Catch.Tests.Android/MainActivity.cs b/osu.Game.Rulesets.Catch.Tests.Android/MainActivity.cs index 64c71c9ecd..d8b729576d 100644 --- a/osu.Game.Rulesets.Catch.Tests.Android/MainActivity.cs +++ b/osu.Game.Rulesets.Catch.Tests.Android/MainActivity.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Android.App; using osu.Framework.Android; using osu.Game.Tests; diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs index b6cb351c1e..baca8166d1 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using Newtonsoft.Json; diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs index cf030f6e13..880316f177 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty; diff --git a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs index b9d6f28228..b74120fa3c 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using NUnit.Framework; using osu.Game.Beatmaps.Legacy; diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs index f30b216d8d..72011042bc 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.IO.Stores; using osu.Game.Rulesets.Catch.Skinning; diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs index 2af851a561..4306cc7d9d 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchEditorTestSceneContainer.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchEditorTestSceneContainer.cs index 39508359a4..058d4eb6b9 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchEditorTestSceneContainer.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchEditorTestSceneContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs index 033dca587e..6dfc74e75c 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs index 2db4102513..ed37ff4ef3 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Testing; diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchDistanceSnapGrid.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchDistanceSnapGrid.cs index 1e057cf3fb..8052b8e3f7 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchDistanceSnapGrid.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs index 5593f3d319..c9ba127569 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Tests.Visual; diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs index 93b24d92fb..75d3c3753a 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Utils; diff --git a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs index 0de992c1df..95b4fdc07e 100644 --- a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs index f21825668f..3261fb656e 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Game.Audio; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs index 402f8f548d..569c69a633 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs index 05d3361dc3..a44575a46e 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs index 01cce88d9d..a82edc1df8 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Tests.Visual; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs index 4c1ba33aa2..5406230359 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Extensions.IEnumerableExtensions; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs index cbf900ebc0..1d2ea4610d 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs index c8979381fe..af38956002 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs index 007f309f3f..23fcd49863 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Rulesets.Catch.Mods; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index 223c4e57fc..fda4136a37 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs index 995daaceb1..de3d9d6530 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs index 4b2873e0a8..1534d91e77 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index f8c43a221e..3c222662f5 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs index c91f07891c..c31a7ca99f 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs index aa66fc8741..871da28142 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -21,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests public partial class TestSceneLegacyBeatmapSkin : LegacyBeatmapSkinColourTest { [Resolved] - private AudioManager audio { get; set; } + private AudioManager audio { get; set; } = null!; [BackgroundDependencyLoader] private void load(OsuConfigManager config) diff --git a/osu.Game.Rulesets.Mania.Tests.Android/MainActivity.cs b/osu.Game.Rulesets.Mania.Tests.Android/MainActivity.cs index 789fc9e22d..518071fd49 100644 --- a/osu.Game.Rulesets.Mania.Tests.Android/MainActivity.cs +++ b/osu.Game.Rulesets.Mania.Tests.Android/MainActivity.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Android.App; using osu.Framework.Android; using osu.Game.Tests; diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs index 2fda012f07..281dec3c79 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs index 0a21098d0d..762238be47 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs index 4b332c3faa..b79bcb7682 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects; diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs index a65f949cec..c75095237e 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mania.Edit.Blueprints; diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index aca555552f..fbc0ed1785 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs index a1f4b234c4..8f623d1fc6 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using NUnit.Framework; @@ -25,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor public partial class TestSceneManiaComposeScreen : EditorClockTestScene { [Resolved] - private SkinManager skins { get; set; } + private SkinManager skins { get; set; } = null!; [Cached] private EditorClipboard clipboard = new EditorClipboard(); diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs index 86e87e7486..9d56d31329 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mania.Edit.Blueprints; diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs index 4ae6cb9c7c..7b0171a9ee 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs index c85583c1fd..ec249f6ae9 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs index 9dee861e66..3a9639e04d 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using NUnit.Framework; using osu.Game.Beatmaps.Legacy; diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs index 7d1a934456..641631d05e 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Replays; diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs index 3bd654e75e..ff1f9e6894 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Game.Rulesets.Mania.Beatmaps; using NUnit.Framework; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs index 0c55cebf0d..465d4a49f0 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs index 25e120edc5..dd494dfc82 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs index 30bd600d9d..ba0c97a121 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs index 3881aae22e..47923d0733 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs index 9cccc2dd86..d4bbc8acb6 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs index 2a9727dbd4..c993ba0e0a 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using osu.Framework.Extensions; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs index 30dd83123d..483c468c1e 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs index 0b9ca42af8..a9d18ba401 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneNote.cs index d049d88ea8..2c978c1148 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneNote.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneNote.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mania.Objects; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs index 6485cbb76b..29c47ca93a 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs index 25e24929c9..d44a38fdec 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs index 0557a201c8..11c3ab3cd3 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.UI.Components; diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs index 9fdd93bcc9..e3846e8213 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Testing; diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs index b96fab9ec0..cb9fcca5b0 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using NUnit.Framework; diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs index 7021c081b7..36ecbdb098 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Reflection; using NUnit.Framework; using osu.Framework.IO.Stores; diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs index 4e50fd924c..073bef5061 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.UI; diff --git a/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs index f497c88bcc..2a8dc715f9 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs index b5655a4579..28cdf8907e 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs index 630fdf7ae2..2265d3d347 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index 912cac4fe4..27cb681300 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.cs index bf54dc3179..e4a28167ec 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs index 931673f337..3d3c35773b 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using osu.Game.Rulesets.Objects; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs index 898b558eb3..48b3ce010f 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Game.Rulesets.Mania.UI; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 63e61f17e3..bbb31ab98c 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs index 01474e6e00..64f8b026c2 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Rulesets.Difficulty; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index 440dec82af..d9f9479247 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs b/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs index df95654319..a67d38b29f 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Mania.Objects; diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index 2c7c84de97..06c825e37d 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Utils; using osu.Game.Rulesets.Difficulty.Preprocessing; diff --git a/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs index 262247e244..e9d26b4aa1 100644 --- a/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs +++ b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Input.Bindings; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs index be1cc9a7fe..6a12ec5088 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs index ef7ce9073c..48dde29a9f 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index 381af8be7f..cb275a9aa4 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -23,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints private readonly EditNotePiece tailPiece; [Resolved] - private IScrollingInfo scrollingInfo { get; set; } + private IScrollingInfo scrollingInfo { get; set; } = null!; protected override bool IsValidForPlacement => HitObject.Duration > 0; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs index cf4bca0030..4c356367d3 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Rulesets.Edit; @@ -17,10 +15,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints where T : ManiaHitObject { [Resolved] - private Playfield playfield { get; set; } + private Playfield playfield { get; set; } = null!; [Resolved] - private IScrollingInfo scrollingInfo { get; set; } + private IScrollingInfo scrollingInfo { get; set; } = null!; protected ScrollingHitObjectContainer HitObjectContainer => ((ManiaPlayfield)playfield).GetColumn(HitObject.Column).HitObjectContainer; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs index d77abca350..b3ec3ef3e4 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs index a1392f09fa..01c7bd502a 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; diff --git a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs index 4a070e70b4..1e9085bb2f 100644 --- a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs +++ b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Graphics; using osuTK; diff --git a/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs b/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs index 960a08eeeb..99e1ce04b1 100644 --- a/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs +++ b/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 2d4b5f718c..9cc84450cc 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -45,13 +43,13 @@ namespace osu.Game.Rulesets.Mania.Edit } [Resolved] - private EditorBeatmap beatmap { get; set; } + private EditorBeatmap beatmap { get; set; } = null!; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [Resolved] - private BindableBeatDivisor beatDivisor { get; set; } + private BindableBeatDivisor beatDivisor { get; set; } = null!; private readonly List grids = new List(); @@ -169,7 +167,7 @@ namespace osu.Game.Rulesets.Mania.Edit private partial class DrawableGridLine : DrawableHitObject { [Resolved] - private IScrollingInfo scrollingInfo { get; set; } + private IScrollingInfo scrollingInfo { get; set; } = null!; private readonly IBindable direction = new Bindable(); diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs index 05d8ccc73f..fb3e2d494e 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects; diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaEditorPlayfield.cs b/osu.Game.Rulesets.Mania/Edit/ManiaEditorPlayfield.cs index 0a697ca986..77e372d1d6 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaEditorPlayfield.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaEditorPlayfield.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; using System.Collections.Generic; diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 5e6ae9bb11..8fdbada04f 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using osu.Framework.Allocation; @@ -16,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Edit public partial class ManiaSelectionHandler : EditorSelectionHandler { [Resolved] - private HitObjectComposer composer { get; set; } + private HitObjectComposer composer { get; set; } = null!; public override bool HandleMovement(MoveSelectionEvent moveEvent) { diff --git a/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs b/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs index 179f920c2f..08ee05ad3f 100644 --- a/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs +++ b/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs index 4b94198c4d..ae9e8bd287 100644 --- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Judgements diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs index 32f9689d7e..d28b7bdf58 100644 --- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Mania/ManiaInputManager.cs b/osu.Game.Rulesets.Mania/ManiaInputManager.cs index 9ad24d6256..a41e72660b 100644 --- a/osu.Game.Rulesets.Mania/ManiaInputManager.cs +++ b/osu.Game.Rulesets.Mania/ManiaInputManager.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; using osu.Framework.Allocation; using osu.Framework.Input.Bindings; diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs index 9117c60dcd..e5c5260a49 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs index ebff5cf4e9..25ad6b997d 100644 --- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Objects; diff --git a/osu.Game.Rulesets.Mania/Objects/Note.cs b/osu.Game.Rulesets.Mania/Objects/Note.cs index 578b46a7aa..0035960c63 100644 --- a/osu.Game.Rulesets.Mania/Objects/Note.cs +++ b/osu.Game.Rulesets.Mania/Objects/Note.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Judgements; diff --git a/osu.Game.Rulesets.Mania/Objects/TailNote.cs b/osu.Game.Rulesets.Mania/Objects/TailNote.cs index d6dc25079a..71a594c6ce 100644 --- a/osu.Game.Rulesets.Mania/Objects/TailNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/TailNote.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Judgements; diff --git a/osu.Game.Rulesets.Mania/Properties/AssemblyInfo.cs b/osu.Game.Rulesets.Mania/Properties/AssemblyInfo.cs index 1bc20f7ef3..ca1f7036c7 100644 --- a/osu.Game.Rulesets.Mania/Properties/AssemblyInfo.cs +++ b/osu.Game.Rulesets.Mania/Properties/AssemblyInfo.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Runtime.CompilerServices; // We publish our internal attributes to other sub-projects of the framework. diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs index 16f7af0d0a..e63a037ca9 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs index c46a1b5ab6..289f8a00ef 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Scoring diff --git a/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs index 765fd11dd5..44ffeb5ec2 100644 --- a/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs +++ b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Input.Bindings; diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index c93be91a84..91e0f2c19b 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.UI; diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs index ef34fc04ee..a5c6f10907 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs index 41b2dba173..2ad6e4f076 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs index 74ddceeeef..bbae055b84 100644 --- a/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; namespace osu.Game.Rulesets.Mania.UI diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs index d4621ab8f3..1183b616f5 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Rulesets.UI; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs b/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs index 56ac38a737..65dc43af0b 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Replays; diff --git a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs index c39e21bace..b6e8fb7191 100644 --- a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Mania.Objects.Drawables; diff --git a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs index 46cba01771..92f471e36b 100644 --- a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs +++ b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; diff --git a/osu.Game.Rulesets.Osu.Tests.Android/MainActivity.cs b/osu.Game.Rulesets.Osu.Tests.Android/MainActivity.cs index 9b4226d5b6..46c60f06a5 100644 --- a/osu.Game.Rulesets.Osu.Tests.Android/MainActivity.cs +++ b/osu.Game.Rulesets.Osu.Tests.Android/MainActivity.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Android.App; using osu.Framework.Android; using osu.Game.Tests; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs index 587bd2de44..a49afd82f3 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs index 41a099e6e9..03ab7ebbf7 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Tests.Visual; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs index 59146bc05e..d14e593587 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Testing; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs index bb29504ec3..37a109de18 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs index 6378097800..0e8673319e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs index c899f58c5a..8468995e86 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; diff --git a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs index bee46da1ba..4c11efcc7c 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using NUnit.Framework; diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 7e995f2dde..cda330afe5 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; diff --git a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs index b4727b3c02..05366e9444 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using NUnit.Framework; using osu.Game.Beatmaps.Legacy; diff --git a/osu.Game.Rulesets.Osu.Tests/StackingTest.cs b/osu.Game.Rulesets.Osu.Tests/StackingTest.cs index a2ab7b564c..e370807bca 100644 --- a/osu.Game.Rulesets.Osu.Tests/StackingTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/StackingTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.IO; using System.Linq; diff --git a/osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.cs b/osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.cs index 5366a86bc0..323df75daf 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs index ff71300733..5da54f8fcf 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -25,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Tests public partial class TestSceneDrawableJudgement : OsuSkinnableTestScene { [Resolved] - private OsuConfigManager config { get; set; } + private OsuConfigManager config { get; set; } = null!; private readonly List> pools; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index 50f9c5e775..0314afc1ac 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -25,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Tests private int depthIndex; [Resolved] - private OsuConfigManager config { get; set; } + private OsuConfigManager config { get; set; } = null!; [Test] public void TestHits() diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs index 34c67e8b9c..b7c3097864 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Game.Rulesets.Osu.Objects; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleHidden.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleHidden.cs index b3498b9651..c113993d31 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleHidden.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs index 93f1123341..bfb31d5b31 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs index 0d3fd77568..c37660831b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoSpinnerStacking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoSpinnerStacking.cs index 1f0e264cf7..30d9aff83a 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoSpinnerStacking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoSpinnerStacking.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHitObjectSamples.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHitObjectSamples.cs index 4d0b2cc406..61cc10f284 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHitObjectSamples.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHitObjectSamples.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Reflection; using NUnit.Framework; using osu.Framework.IO.Stores; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs index 53c4e49807..44efc94d7b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Tests.Visual; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs index b66974d4b1..0bb27cff0f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs index bee7831625..0599517899 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Diagnostics; using osu.Framework.Threading; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderComboChange.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderComboChange.cs index dc8842a20a..863cc24920 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderComboChange.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderComboChange.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderHidden.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderHidden.cs index eb13995ad0..671285831f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderHidden.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerHidden.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerHidden.cs index 1aaba23e56..42cbb96813 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerHidden.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs index df146a9511..a5282877ee 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index d03ee81f0d..9e786caf0c 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs index da66669550..f51f04bf87 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics; using osu.Game.Beatmaps; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index 6d1b4d1a15..cf35b08822 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index dabbfcd2fb..5cb5a8f934 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 3bec2346ce..05939bb3ab 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index c98f875eb5..1ae500ec78 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index efb3ade220..0aeaf7669f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Rulesets.Difficulty; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 30b56ff769..b31f4ff519 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 6aea00fd35..5215920ea0 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 38e0e5b677..3f6b22bbb1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 40448c444c..3d6d3f99c1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index d6683ade05..15b20a5572 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Skills; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index efe0e136bf..40aac013ab 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/BlueprintPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/BlueprintPiece.cs index 41ab5a81b8..cdd11c53d2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/BlueprintPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/BlueprintPiece.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Objects; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs index 1fed19da03..670e98ca50 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 26d18c7a17..20ad99baa2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs index 1b3e7f3a8f..0608f8c929 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs index d6409279a4..bdd19f9106 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -17,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints where T : OsuHitObject { [Resolved] - private EditorClock editorClock { get; set; } + private EditorClock editorClock { get; set; } = null!; protected new DrawableOsuHitObject DrawableObject => (DrawableOsuHitObject)base.DrawableObject; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs index 68a44eb2f8..075e9e6aa1 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Allocation; using osu.Game.Graphics; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs index 3341a632c1..d47cf6bf23 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs index cc58acdc80..17e838b4e7 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs index a80ec68c10..b273292f8a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components; using osu.Game.Rulesets.Osu.Objects; using osuTK; diff --git a/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs index 69187875d7..c41ae10b2e 100644 --- a/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs +++ b/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs index 2c67f0bf97..325e9ed4cb 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs index 173a664902..ed149d004c 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs index 3234b03a3e..efc6668ebf 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Input.Bindings; @@ -22,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Edit private int currentGridSizeIndex = grid_sizes.Length - 1; [Resolved] - private EditorBeatmap editorBeatmap { get; set; } + private EditorBeatmap editorBeatmap { get; set; } = null!; public OsuRectangularPositionSnapGrid() : base(OsuPlayfield.BASE_SIZE / 2) diff --git a/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs index 0a3fc176ad..676205c8d7 100644 --- a/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs +++ b/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs index 3c0cf34010..c8160617c9 100644 --- a/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs +++ b/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Osu/Judgements/ComboResult.cs b/osu.Game.Rulesets.Osu/Judgements/ComboResult.cs index 9762c676c5..556eb94f38 100644 --- a/osu.Game.Rulesets.Osu/Judgements/ComboResult.cs +++ b/osu.Game.Rulesets.Osu/Judgements/ComboResult.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; namespace osu.Game.Rulesets.Osu.Judgements diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs index 5f9faaceb2..7a9a868b9b 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs index 1bdb74cd3b..7c7f16779e 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Judgements diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs index a5503d3273..1a88e2a8b2 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuJudgementResult.cs index 50d73fa19d..3bc2cacb43 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuJudgementResult.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuJudgementResult.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs index 4229c87b58..941cb667cf 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; diff --git a/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs index 270c1f31fb..21b024a720 100644 --- a/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs +++ b/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Judgements diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs index d588127cb9..52edfb1422 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osuTK; using osuTK.Graphics; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs index f1f4ec983e..889a9bd816 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Rulesets.Osu.Objects.Drawables { public partial class DrawableSpinnerBonusTick : DrawableSpinnerTick diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/ITrackSnaking.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/ITrackSnaking.cs index 9e8035a1ee..cae2a7c36d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/ITrackSnaking.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/ITrackSnaking.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables diff --git a/osu.Game.Rulesets.Osu/Objects/HitCircle.cs b/osu.Game.Rulesets.Osu/Objects/HitCircle.cs index 5f43e57ed8..d652db0fd4 100644 --- a/osu.Game.Rulesets.Osu/Objects/HitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/HitCircle.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 7b98fc48e0..fd5741698a 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using osu.Framework.Bindables; diff --git a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs index f52c3ab382..ddbbb300ca 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs index 2a84b04030..73c222653e 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; diff --git a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs index 7b9316f8ac..cca86361c2 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index 87c8117b6b..b4574791d2 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index 676ff62455..74ec4d6eb3 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index ba0981e781..b800b03c92 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs index 00ceccaf7b..8d53100529 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs index c890f3771b..7989c9b7ff 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Osu/Properties/AssemblyInfo.cs b/osu.Game.Rulesets.Osu/Properties/AssemblyInfo.cs index 7fffb1871f..c842874635 100644 --- a/osu.Game.Rulesets.Osu/Properties/AssemblyInfo.cs +++ b/osu.Game.Rulesets.Osu/Properties/AssemblyInfo.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Runtime.CompilerServices; // We publish our internal attributes to other sub-projects of the framework. diff --git a/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs index 0dac3307c2..b509796742 100644 --- a/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs index b45d552c7f..2c3a77a6bc 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.UI; diff --git a/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs b/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs index 66a4f467a9..545b31bf29 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Replays; diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs index aa4cd0af14..e936c24c08 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; diff --git a/osu.Game.Rulesets.Taiko.Tests.Android/MainActivity.cs b/osu.Game.Rulesets.Taiko.Tests.Android/MainActivity.cs index a55b461876..e4f4bbfd53 100644 --- a/osu.Game.Rulesets.Taiko.Tests.Android/MainActivity.cs +++ b/osu.Game.Rulesets.Taiko.Tests.Android/MainActivity.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Android.App; using osu.Framework.Android; using osu.Game.Tests; diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs index 157a96eec8..68517166e7 100644 --- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs index 747c599721..878f451fd2 100644 --- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Input.Events; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditor.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditor.cs index 3ee9171e7e..822219db29 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditor.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditor.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Tests.Visual; diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoEditorSaving.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoEditorSaving.cs index 93b26624de..af7db2251b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoEditorSaving.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoEditorSaving.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Rulesets.Taiko.Beatmaps; diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs index ed73730c4a..64a29ce866 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TaikoSkinnableTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TaikoSkinnableTestScene.cs index 38530282b7..b11501535f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TaikoSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TaikoSkinnableTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests.Skinning diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs index 7fd90685e3..497c788ce8 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs index 3b2f4f2fb2..bef8be4cb5 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs index 9567eac80f..863ca4a07d 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs index 924f903ce9..f8dd53c834 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs index cb5d0d1f91..3c6319ddf9 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs index 5f98f2f27a..fbdc4132ec 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs index eb2762cb2d..c89e2b727b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using NUnit.Framework; using osu.Framework.Allocation; diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs index 826cf2acab..2535058c29 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Testing; using osu.Framework.Timing; diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs index 781a686700..a528a7f9d1 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using NUnit.Framework; diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index 5685ac0f60..09d6540f72 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs index c86f8cb8d2..541987d63e 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using NUnit.Framework; using osu.Game.Beatmaps.Legacy; diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs index 00292d5473..44bc611d92 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs index b01bd11149..0a178ec69f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs index 301620edc9..24aa4f2ef0 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index 91209e5ec5..fd850a9a67 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs index dcdda6014c..2429b71095 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Testing; diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.cs index 8c903f748c..d2cf691c7f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs index 08c06b08f2..9e45197b04 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Game.Beatmaps; diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs index 6ff5cdf7e5..41fe63a553 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 1c2e7abafe..dddd1e3c5a 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index ff187a133a..e76af13686 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index d04c028fec..e528c70699 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 24b5f5939a..8e988c4154 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs index b61c13a2df..b12c0ca29d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Rulesets.Difficulty; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 2d1b2903c9..a193bacde5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs index 4b4e2b5847..2f3c722fda 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Edit.Blueprints diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs index 84bc547372..3401f944e4 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs index af02522a05..a56f92a026 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs index 2080293428..9b5ef640d7 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Edit.Blueprints diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs index 34695cbdd6..93b7c7061b 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; diff --git a/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs index acb17fc455..f332441875 100644 --- a/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs +++ b/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs index e52dae4b0c..fa50841893 100644 --- a/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs +++ b/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs index dd0ff61c10..4d4ee8effe 100644 --- a/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs +++ b/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs index 6be22f3af0..027723c02c 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Edit.Blueprints; diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index b727c0a61b..7ab8a54b02 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs index de56c76f56..4fc455fb23 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.Judgements diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs index f8e3303752..e272c1a4ef 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs index bafe7dfbaf..1fc50c8751 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.Judgements diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs index 146621997d..d22ac6bf5e 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.Judgements diff --git a/osu.Game.Rulesets.Taiko/Objects/BarLine.cs b/osu.Game.Rulesets.Taiko/Objects/BarLine.cs index d2eba0eb54..46b3f13501 100644 --- a/osu.Game.Rulesets.Taiko/Objects/BarLine.cs +++ b/osu.Game.Rulesets.Taiko/Objects/BarLine.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs index 0cd265ecab..a039ce3407 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index 4ea30453d1..6ee29fa014 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using JetBrains.Annotations; using osu.Game.Rulesets.Taiko.Judgements; diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs index 206e8ecb5a..eb333d9ec5 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Judgements; diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs index ec23079ed9..156e890607 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Hit.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; diff --git a/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs b/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs index 18f47b7cff..302f940ef4 100644 --- a/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; namespace osu.Game.Rulesets.Taiko.Objects diff --git a/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs index 316115f44d..14cbe338ed 100644 --- a/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Judgements; diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs index 9ad783ba7e..a8db8df021 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Swell.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Threading; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Judgements; diff --git a/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs index 43830cb528..41fb9cac7e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs index 3aba5c571b..1a1fde1990 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs index 479ad8369a..228179f94b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using System.Threading; using osu.Framework.Bindables; diff --git a/osu.Game.Rulesets.Taiko/Properties/AssemblyInfo.cs b/osu.Game.Rulesets.Taiko/Properties/AssemblyInfo.cs index 5b66e18a6d..ca7d04876e 100644 --- a/osu.Game.Rulesets.Taiko/Properties/AssemblyInfo.cs +++ b/osu.Game.Rulesets.Taiko/Properties/AssemblyInfo.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Runtime.CompilerServices; // We publish our internal attributes to other sub-projects of the framework. diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index 7c70beb0a4..d75906f3aa 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using osu.Game.Beatmaps; diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs index 896af24772..cf806c0c97 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.Scoring diff --git a/osu.Game.Rulesets.Taiko/TaikoInputManager.cs b/osu.Game.Rulesets.Taiko/TaikoInputManager.cs index ca06a0a77e..4292d461bf 100644 --- a/osu.Game.Rulesets.Taiko/TaikoInputManager.cs +++ b/osu.Game.Rulesets.Taiko/TaikoInputManager.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; using osu.Framework.Allocation; using osu.Framework.Input.Bindings; diff --git a/osu.Game.Tests.Android/MainActivity.cs b/osu.Game.Tests.Android/MainActivity.cs index bdb947fbb4..ab43a6766c 100644 --- a/osu.Game.Tests.Android/MainActivity.cs +++ b/osu.Game.Tests.Android/MainActivity.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Reflection; using Android.App; using Android.OS; diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index fac5e098b9..5d9049ead7 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections; using System.Collections.Generic; diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyDecoderTest.cs index c1c9e0d118..39bb616563 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyDecoderTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps.Formats; diff --git a/osu.Game.Tests/Beatmaps/IO/LineBufferedReaderTest.cs b/osu.Game.Tests/Beatmaps/IO/LineBufferedReaderTest.cs index 8f20fd7a68..5e37f01c81 100644 --- a/osu.Game.Tests/Beatmaps/IO/LineBufferedReaderTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/LineBufferedReaderTest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.IO; using System.Text; diff --git a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs index 04eb9a3fa2..810ea5dbd0 100644 --- a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.IO; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs index d30ab3dea1..a26c8121dd 100644 --- a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs +++ b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Game.Rulesets.Objects; diff --git a/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs b/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs index b4a205b478..f3c05d8970 100644 --- a/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs +++ b/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Models; diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs index 9079ecdc48..d034e69957 100644 --- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs +++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.IO; using System.Linq; diff --git a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs index e7fdb52d2f..b237556d11 100644 --- a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs +++ b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.IO; using System.Linq; diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs index 769ca1f9a9..d198ef5074 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.IO.Stores; using osu.Game.Rulesets; diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs index a261185473..1cf72cf937 100644 --- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game.Tests/ImportTest.cs b/osu.Game.Tests/ImportTest.cs index 9e2c9cd7e0..3f0f8a4f14 100644 --- a/osu.Game.Tests/ImportTest.cs +++ b/osu.Game.Tests/ImportTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Threading; using System.Threading.Tasks; diff --git a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs index 3c296b2ff5..6b43ab83c5 100644 --- a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs +++ b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; @@ -18,7 +16,7 @@ namespace osu.Game.Tests.Input public partial class ConfineMouseTrackerTest : OsuGameTestScene { [Resolved] - private FrameworkConfigManager frameworkConfigManager { get; set; } + private FrameworkConfigManager frameworkConfigManager { get; set; } = null!; [TestCase(WindowMode.Windowed)] [TestCase(WindowMode.Borderless)] diff --git a/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs b/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs index d01eaca714..9926acf772 100644 --- a/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs +++ b/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Beatmaps; diff --git a/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs b/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs index e7827a7398..0f5c13ca0e 100644 --- a/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs +++ b/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Utils; diff --git a/osu.Game.Tests/NonVisual/ClosestBeatDivisorTest.cs b/osu.Game.Tests/NonVisual/ClosestBeatDivisorTest.cs index 8ebf34b1ca..8a53759323 100644 --- a/osu.Game.Tests/NonVisual/ClosestBeatDivisorTest.cs +++ b/osu.Game.Tests/NonVisual/ClosestBeatDivisorTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps; diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs index a2ded643fa..2d5d425ee8 100644 --- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs +++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Game.Beatmaps.ControlPoints; diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index 6637d640b2..6b1b883ce7 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game.Tests/NonVisual/FormatUtilsTest.cs b/osu.Game.Tests/NonVisual/FormatUtilsTest.cs index 4d2fc53bc3..a12658bd8b 100644 --- a/osu.Game.Tests/NonVisual/FormatUtilsTest.cs +++ b/osu.Game.Tests/NonVisual/FormatUtilsTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Utils; diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index ae6a76f6cd..b4bbe274a5 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using Humanizer; using NUnit.Framework; diff --git a/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs b/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs index b589f7c9f1..664a499cc3 100644 --- a/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs +++ b/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.cs b/osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.cs index 8654abd49d..335e7d25a2 100644 --- a/osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.cs +++ b/osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Game.Rulesets; diff --git a/osu.Game.Tests/NonVisual/ScoreInfoTest.cs b/osu.Game.Tests/NonVisual/ScoreInfoTest.cs index dcc4f91dba..ad3b5b6f66 100644 --- a/osu.Game.Tests/NonVisual/ScoreInfoTest.cs +++ b/osu.Game.Tests/NonVisual/ScoreInfoTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Online.API; using osu.Game.Rulesets.Mania; diff --git a/osu.Game.Tests/NonVisual/TimeDisplayExtensionTest.cs b/osu.Game.Tests/NonVisual/TimeDisplayExtensionTest.cs index 861e342cdb..10d592364d 100644 --- a/osu.Game.Tests/NonVisual/TimeDisplayExtensionTest.cs +++ b/osu.Game.Tests/NonVisual/TimeDisplayExtensionTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using NUnit.Framework; using osu.Game.Extensions; diff --git a/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs b/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs index 13f1ed5c57..e4118a23b4 100644 --- a/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs +++ b/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Online.Chat; diff --git a/osu.Game.Tests/Online/TestMultiplayerMessagePackSerialization.cs b/osu.Game.Tests/Online/TestMultiplayerMessagePackSerialization.cs index aea579a82d..c440f375fd 100644 --- a/osu.Game.Tests/Online/TestMultiplayerMessagePackSerialization.cs +++ b/osu.Game.Tests/Online/TestMultiplayerMessagePackSerialization.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using MessagePack; using NUnit.Framework; using osu.Game.Online; diff --git a/osu.Game.Tests/Online/TestSoloScoreInfoJsonSerialization.cs b/osu.Game.Tests/Online/TestSoloScoreInfoJsonSerialization.cs index 8ff0b67b5b..19bc96c677 100644 --- a/osu.Game.Tests/Online/TestSoloScoreInfoJsonSerialization.cs +++ b/osu.Game.Tests/Online/TestSoloScoreInfoJsonSerialization.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Newtonsoft.Json; using NUnit.Framework; using osu.Game.IO.Serialization; diff --git a/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs b/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs index 73ed2bb868..274681b413 100644 --- a/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs +++ b/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using NUnit.Framework; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game.Tests/Scores/IO/TestScoreEquality.cs b/osu.Game.Tests/Scores/IO/TestScoreEquality.cs index d44fd786d7..5aa07260ef 100644 --- a/osu.Game.Tests/Scores/IO/TestScoreEquality.cs +++ b/osu.Game.Tests/Scores/IO/TestScoreEquality.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using NUnit.Framework; using osu.Game.Scoring; diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index 0c25934d52..1c1ebe271e 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.IO; using System.Linq; diff --git a/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs index 6da335a9b7..b96bf09255 100644 --- a/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs +++ b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.IO; using osu.Game.Skinning; diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs index 9f3e36ad76..9e5bd53b13 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs index 8a11d60875..88e47ea560 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game.Tests/Visual/Colours/TestSceneStarDifficultyColours.cs b/osu.Game.Tests/Visual/Colours/TestSceneStarDifficultyColours.cs index 55a2efa89d..7f07563dfd 100644 --- a/osu.Game.Tests/Visual/Colours/TestSceneStarDifficultyColours.cs +++ b/osu.Game.Tests/Visual/Colours/TestSceneStarDifficultyColours.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -19,7 +17,7 @@ namespace osu.Game.Tests.Visual.Colours public partial class TestSceneStarDifficultyColours : OsuTestScene { [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [Test] public void TestColours() diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs index 8b598a6a24..fdb3513e66 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Testing; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs index e11d2e9dbf..2a822b3f1f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs index 699b99c57f..dbcf66f005 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs index a9d054881b..c0d64f4030 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs index b2b3dd9632..da4f159cae 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs index f255dd08a8..ddca2f8553 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs index c874b39028..c78f50c821 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using Humanizer; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index b0b51a5dbd..29de0bff79 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using System.Collections.Generic; using Humanizer; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs index c0e681b8b4..534b813ddc 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs index b63296a48d..79ca8ee20c 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Edit.Compose.Components.Timeline; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs index 08e036248b..7a5243f6e8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs index 41fb3ed8b9..1376ba23fb 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs index 19f4678c15..b493845ad4 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Gameplay/OsuPlayerTestScene.cs b/osu.Game.Tests/Visual/Gameplay/OsuPlayerTestScene.cs index 7ff059ff77..7252fbc474 100644 --- a/osu.Game.Tests/Visual/Gameplay/OsuPlayerTestScene.cs +++ b/osu.Game.Tests/Visual/Gameplay/OsuPlayerTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs index 6cb1101173..b251253b7c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using NUnit.Framework; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index e779c6c1cb..6297b062dd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using osu.Game.Rulesets; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 22f7111f68..f978653f2a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs index 626406e4d2..71ed0a14a2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Overlays; using osu.Game.Users; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs index 84334ba0a9..0e03f253a9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps.Timing; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs index 2ae5e6f998..515bc09bbb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs index 6f079778c5..59a1f938e6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs index 93fa953ef4..0c351a93bb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs index 7f6c9d7804..b4ebb7c410 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs index 2cb3303dd6..7079b93d3e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs index 1c09c29748..f1ee5cc414 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Screens; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs index 699c8ea20a..b002e90bb0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Utils; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneUnknownMod.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneUnknownMod.cs index cb5631e599..1d85643adf 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneUnknownMod.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneUnknownMod.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; diff --git a/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs b/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs index 45e5a7c270..fb82b0df80 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs index 0c024248ea..c0ced9057a 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Screens.Menu; diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs index 23373892d1..e8859400d5 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Screens.Menu; diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs index 2ccf184525..cb4a52a3b9 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Screens.Menu; diff --git a/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs b/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs index e5e092b382..4f01dcffd9 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs b/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs index c54c66df7e..e4add64da2 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays; diff --git a/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs b/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs index 49256c7a01..f4732234a7 100644 --- a/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs +++ b/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index 63a0ada3dc..24d1b51ff8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Online.API; using osu.Game.Online.Rooms; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs index defb3006cc..ea8fe8873d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Online.API; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs index 3d85a47ca9..46d409e6f1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Screens; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index a612167d57..bafe373d57 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs index 48f74cf308..37662ffce8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Graphics; using osu.Game.Online.Multiplayer; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs index d636373fbd..c2d3b17ccb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs index aaf1a850af..d5f53bc354 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using Moq; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs index e46ae978d7..b53a61f881 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Beatmaps; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneButtonSystemNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneButtonSystemNavigation.cs index 64ea6003bc..c6d67f2bc6 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneButtonSystemNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneButtonSystemNavigation.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Testing; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs b/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs index bd75825da2..1633a778ab 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs index f885c2f44c..0bfe02cb16 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupRuleset.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupRuleset.cs index 621dabe869..d70eaf16f6 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneStartupRuleset.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupRuleset.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Development; using osu.Game.Configuration; diff --git a/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs b/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs index c32aa7f5f9..820df02b66 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Configuration; diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.cs index 36f8d2d9bd..ccd3d7f4d6 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index d9763ef6c8..60fb6b8c86 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Beatmaps; @@ -39,7 +37,7 @@ namespace osu.Game.Tests.Visual.Online } [Resolved] - private IRulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } = null!; [SetUp] public void SetUp() => Schedule(() => SelectedMods.Value = Array.Empty()); diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogSupporterPromo.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogSupporterPromo.cs index 96996db940..fbd3b3a728 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogSupporterPromo.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogSupporterPromo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsHeader.cs index 43d80ee0ac..89b8a8c079 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentsHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsHeader.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs index 504be45b44..9407941da6 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Overlays; diff --git a/osu.Game.Tests/Visual/Online/TestSceneExternalLinkButton.cs b/osu.Game.Tests/Visual/Online/TestSceneExternalLinkButton.cs index 90ec3160d8..84f245afb6 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneExternalLinkButton.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneExternalLinkButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game.Tests/Visual/Online/TestSceneGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneGraph.cs index 357ed7548c..4f19003638 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneGraph.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneGraph.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs index a58845ca7e..5c726bd69e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics.Containers; using osu.Framework.Graphics; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game.Tests/Visual/Online/TestSceneLeaderboardScopeSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneLeaderboardScopeSelector.cs index 0231775189..4c67f778a2 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneLeaderboardScopeSelector.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneLeaderboardScopeSelector.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Overlays.BeatmapSet; using osu.Framework.Graphics; using osu.Framework.Bindables; diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs index 001e6d925e..2d91df8a6d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics.Containers; using osu.Framework.Graphics; using osu.Game.Overlays.News; diff --git a/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapListingOverlay.cs index ecfa76f395..f332575fb4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapListingOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Overlays; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapSetOverlay.cs index 01b0b39661..3a4d0b97db 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapSetOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Overlays; diff --git a/osu.Game.Tests/Visual/Online/TestSceneOnlineViewContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneOnlineViewContainer.cs index 6c8430e955..df29b33f17 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneOnlineViewContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneOnlineViewContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsCountryFilter.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsCountryFilter.cs index dfefcd735e..3b38cf47d8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankingsCountryFilter.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsCountryFilter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Overlays.Rankings; diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs index 5aef91bef1..eb2a451c9b 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Overlays; diff --git a/osu.Game.Tests/Visual/Online/TestSceneSpotlightsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneSpotlightsLayout.cs index 4cbcaaac85..5ce82a8766 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneSpotlightsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneSpotlightsLayout.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Online/TestSceneTotalCommentsCounter.cs b/osu.Game.Tests/Visual/Online/TestSceneTotalCommentsCounter.cs index 8af87dd597..cbd8ffa91c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneTotalCommentsCounter.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneTotalCommentsCounter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Bindables; using osu.Game.Overlays.Comments; diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMainPage.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMainPage.cs index 8876f0fd3b..9967be73e8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiMainPage.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMainPage.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs index c4a1200cb1..3b60c28dc0 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs index 71e284ecfe..891dd3bb1a 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; namespace osu.Game.Tests.Visual.Playlists diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index bf18bd3e51..03b168c72c 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using NUnit.Framework; using osu.Framework.Extensions.Color4Extensions; diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs index e92e74598d..3004cb8a0c 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs index be7be6d4f1..27d66ea2a2 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs index ce6973aacf..3ef0ffc13a 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Tests.Visual.UserInterface; diff --git a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs index f61e3ca557..e8f74a2f1b 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -16,7 +14,7 @@ namespace osu.Game.Tests.Visual.Settings public partial class TestSceneFileSelector : ThemeComparisonTestScene { [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [Test] public void TestJpgFilesOnly() diff --git a/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs b/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs index 91320fdb1c..22f185cbd3 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.IO; using System.Threading; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs index 30811bab32..309438e51c 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs index 46a26d2e98..fa4981c137 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs index 0476198e41..30f1803795 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs index 837de60053..494268b158 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs index dd7bf48791..343378ccfb 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs index 7f7ba6966b..5e22450ba8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBreadcrumbControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBreadcrumbControl.cs index eeb2d1e70f..92bf2448b1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBreadcrumbControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBreadcrumbControl.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneColourPicker.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneColourPicker.cs index d2acf89dc8..bc3f1d0070 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneColourPicker.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneColourPicker.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs index 1bfa389a25..eaaf40fb36 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Overlays.Comments.Buttons; using osu.Framework.Graphics; using osu.Framework.Allocation; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs index 3491b7dbc1..7b80549854 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs index 01d4eb83f3..a8eaabe758 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs index 6092f35050..77658dc482 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics.Containers; using osu.Framework.Graphics; using osu.Game.Overlays.Dashboard.Home; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDrawableDate.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDrawableDate.cs index 108ad8b7c1..b590abf4e5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDrawableDate.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDrawableDate.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneEditorSidebar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneEditorSidebar.cs index 72dacb7558..7d1b3a4bb7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneEditorSidebar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneEditorSidebar.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingBar.cs index 9d850c0fc5..ed0fd340e7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingBar.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBehaviour.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBehaviour.cs index ec8ef0ad50..4b589ffe4c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBehaviour.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBehaviour.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Game.Overlays; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBundledBeatmaps.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBundledBeatmaps.cs index e9460e45d3..30a36652c2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBundledBeatmaps.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBundledBeatmaps.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Game.Overlays; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenImportFromStable.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenImportFromStable.cs index e6fc889a70..680b54f637 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenImportFromStable.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenImportFromStable.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Threading; using System.Threading.Tasks; using Moq; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenUIScale.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenUIScale.cs index 8ba94cf9ae..2dee57f4cb 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenUIScale.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenUIScale.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Game.Overlays; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs index 24b4060a42..4e1bf1390a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs index 801bef62c8..91c1f58155 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneIconButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneIconButton.cs index 454fa7cd05..a1910570dd 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneIconButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneIconButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDropdown.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDropdown.cs index a2cfae3c7f..300b451cf5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDropdown.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDropdown.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterfaceV2; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs index 6181891e13..726f13861b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSwitchButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSwitchButton.cs index c4af47bd0f..bec517af2c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSwitchButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSwitchButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs index 8046554819..a9f6b812df 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingSpinner.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingSpinner.cs index 40e786592a..bd36be846b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingSpinner.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingSpinner.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs index f9d92aabc6..863e59f6a7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModDisplay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModDisplay.cs index bd5a0d8645..1bb83eeddf 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModDisplay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Rulesets.Mods; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchSmall.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchSmall.cs index 07312379b3..83dc96d0d7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchSmall.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchSmall.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs index 34dd139428..2d953fe9ad 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs index f2123061e5..4bd3a883f1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs index 770b9dece1..b0548d7e9f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs index 24a27f71e8..62a493815b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Menu; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuPopover.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuPopover.cs index 4ff7befe71..98ae50c915 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuPopover.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuPopover.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs index 929537e675..69fe8ad105 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs index e90041774e..a927b0931b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics.Containers; using osu.Game.Overlays; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeaderBackground.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeaderBackground.cs index 7a445427f5..5a43c5ae69 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeaderBackground.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeaderBackground.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics.Containers; using osu.Game.Overlays; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayRulesetSelector.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayRulesetSelector.cs index 432e448038..d83e922edf 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayRulesetSelector.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayRulesetSelector.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePageSelector.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePageSelector.cs index b9e3592389..3f3b6d8267 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePageSelector.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePageSelector.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneParallaxContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneParallaxContainer.cs index 92d4981d4a..38f65b3a17 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneParallaxContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneParallaxContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Game.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRankingsSortTabControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRankingsSortTabControl.cs index f364a48616..59600e639f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneRankingsSortTabControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRankingsSortTabControl.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Overlays; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs index cbf67c49a6..8c2651f71d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Bindables; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenBreadcrumbControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenBreadcrumbControl.cs index 3d35f2c9cc..968cf9f9db 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenBreadcrumbControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenBreadcrumbControl.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs index a0fe5fce32..1101f27139 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayHeader.cs index aeea0681eb..27b128e709 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayHeader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSearchTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSearchTextBox.cs index 0072864335..f3a7f1481a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSearchTextBox.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSearchTextBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneTabControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneTabControl.cs index 24c4ed79b1..94117ff7e3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneTabControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneTabControl.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneToggleMenuItem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneToggleMenuItem.cs index 41a6f35624..f564f561ec 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneToggleMenuItem.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneToggleMenuItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneTwoLayerButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneTwoLayerButton.cs index 20b0ab5801..524119f34d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneTwoLayerButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneTwoLayerButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs index 8737f7312e..a373fbbc51 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.cs index 7cedef96e3..311bae0d50 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Overlays.Volume; using osuTK; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneWaveContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneWaveContainer.cs index 7851571b36..e1f8357a36 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneWaveContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneWaveContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; diff --git a/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs index 05ffd1fbef..2c894eacab 100644 --- a/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs +++ b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs index cb923a1f9a..3dac550bd6 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Tournament.Models; diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs index dd7c613c6c..a809d0747a 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Tests.Visual; diff --git a/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs b/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs index 2347c84ba8..ae4c5ec685 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; diff --git a/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs index 9b1fc17591..3007232077 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Utils; diff --git a/osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs index cb22e7e7c7..431b6eff63 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs index 057566d426..4fa90437c8 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Online.API; @@ -21,7 +19,7 @@ namespace osu.Game.Tournament.Tests.Components /// It cannot be trivially replaced because setting to causes to no longer be usable. /// [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs index d9ae8df651..ddae626305 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index 45dffdc94a..3ae1400a99 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.IO; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tournament.Tests/NonVisual/LadderInfoSerialisationTest.cs b/osu.Game.Tournament.Tests/NonVisual/LadderInfoSerialisationTest.cs index f1e0966293..962a6fbd6c 100644 --- a/osu.Game.Tournament.Tests/NonVisual/LadderInfoSerialisationTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/LadderInfoSerialisationTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Newtonsoft.Json; using NUnit.Framework; using osu.Game.Tournament.Models; diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.cs index 10ed850002..cd15550931 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.IO; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs index 5c4e1b2a5a..839730f3ca 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics.Cursor; diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneLadderScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneLadderScreen.cs index 20f729bb8d..ecc741ddc1 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneLadderScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneLadderScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics.Cursor; diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneRoundEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneRoundEditorScreen.cs index ebeb69012d..2e7bf364f7 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneRoundEditorScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneRoundEditorScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Tournament.Screens.Editors; namespace osu.Game.Tournament.Tests.Screens diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs index cfb533149d..0d06b0352f 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.Editors; diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs index c9620bc0b9..f656934239 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs index 84c8b9a141..6af2af7732 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Tournament.Screens.Setup; diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneShowcaseScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneShowcaseScreen.cs index 6287679c27..e50a2a0c07 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneShowcaseScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneShowcaseScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Tournament.Screens.Showcase; diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs index dbd9cb2817..db49bc960a 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Tournament.Screens.Setup; namespace osu.Game.Tournament.Tests.Screens diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneTeamEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneTeamEditorScreen.cs index 63c08800ad..7b371ed78b 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneTeamEditorScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneTeamEditorScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Tournament.Screens.Editors; namespace osu.Game.Tournament.Tests.Screens diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs index 5c26bc203c..02762ab5c7 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs index 43e16873c6..8096988864 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; diff --git a/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs b/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs index 859d0591c3..f580b2e455 100644 --- a/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs +++ b/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; namespace osu.Game.Tournament.Tests diff --git a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs index f29272fbb8..037afd8690 100644 --- a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs +++ b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; diff --git a/osu.Game.Tournament.Tests/TournamentTestRunner.cs b/osu.Game.Tournament.Tests/TournamentTestRunner.cs index f95fcbf487..f6aef53514 100644 --- a/osu.Game.Tournament.Tests/TournamentTestRunner.cs +++ b/osu.Game.Tournament.Tests/TournamentTestRunner.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework; using osu.Framework.Platform; diff --git a/osu.Game.Tournament/Components/ControlPanel.cs b/osu.Game.Tournament/Components/ControlPanel.cs index c3e66e80eb..b5912349a0 100644 --- a/osu.Game.Tournament/Components/ControlPanel.cs +++ b/osu.Game.Tournament/Components/ControlPanel.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs index 192d8c9fd1..4fa94b6c63 100644 --- a/osu.Game.Tournament/Components/DateTextBox.cs +++ b/osu.Game.Tournament/Components/DateTextBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Bindables; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game.Tournament/Components/DrawableTeamHeader.cs b/osu.Game.Tournament/Components/DrawableTeamHeader.cs index 1648e7373b..e9ce9f3759 100644 --- a/osu.Game.Tournament/Components/DrawableTeamHeader.cs +++ b/osu.Game.Tournament/Components/DrawableTeamHeader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Tournament.Models; using osuTK; diff --git a/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs index 27113b0d21..59e261a7dd 100644 --- a/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs +++ b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Tournament.Models; diff --git a/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs index 9606670ad8..5ebed34e6a 100644 --- a/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs +++ b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs index c83fceb01d..671d2ffb65 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs index 7a1f448cb4..ef576f5b02 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tournament/Components/IPCErrorDialog.cs b/osu.Game.Tournament/Components/IPCErrorDialog.cs index 995bbffffc..07aeb65ee6 100644 --- a/osu.Game.Tournament/Components/IPCErrorDialog.cs +++ b/osu.Game.Tournament/Components/IPCErrorDialog.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics.Sprites; using osu.Game.Overlays.Dialog; diff --git a/osu.Game.Tournament/Components/RoundDisplay.cs b/osu.Game.Tournament/Components/RoundDisplay.cs index 6018cc6ffb..4c72209d12 100644 --- a/osu.Game.Tournament/Components/RoundDisplay.cs +++ b/osu.Game.Tournament/Components/RoundDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; diff --git a/osu.Game.Tournament/Components/TournamentModIcon.cs b/osu.Game.Tournament/Components/TournamentModIcon.cs index 76b6151519..28114e64fe 100644 --- a/osu.Game.Tournament/Components/TournamentModIcon.cs +++ b/osu.Game.Tournament/Components/TournamentModIcon.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -23,7 +21,7 @@ namespace osu.Game.Tournament.Components private readonly string modAcronym; [Resolved] - private IRulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } = null!; public TournamentModIcon(string modAcronym) { diff --git a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs index 3a16662463..97cb610021 100644 --- a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs +++ b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game.Tournament/Configuration/TournamentConfigManager.cs b/osu.Game.Tournament/Configuration/TournamentConfigManager.cs index 8f256ba9c3..b76b18cb5f 100644 --- a/osu.Game.Tournament/Configuration/TournamentConfigManager.cs +++ b/osu.Game.Tournament/Configuration/TournamentConfigManager.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Configuration; using osu.Framework.Platform; diff --git a/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs b/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs index ba584f1d3e..ba0e593eae 100644 --- a/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs +++ b/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.IO.Stores; using osu.Framework.Platform; diff --git a/osu.Game.Tournament/IPC/MatchIPCInfo.cs b/osu.Game.Tournament/IPC/MatchIPCInfo.cs index 3bf790d58e..ff557717d5 100644 --- a/osu.Game.Tournament/IPC/MatchIPCInfo.cs +++ b/osu.Game.Tournament/IPC/MatchIPCInfo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps.Legacy; diff --git a/osu.Game.Tournament/Models/BeatmapChoice.cs b/osu.Game.Tournament/Models/BeatmapChoice.cs index ddd4597722..c8ba989fa7 100644 --- a/osu.Game.Tournament/Models/BeatmapChoice.cs +++ b/osu.Game.Tournament/Models/BeatmapChoice.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using Newtonsoft.Json; using Newtonsoft.Json.Converters; diff --git a/osu.Game.Tournament/Models/LadderEditorInfo.cs b/osu.Game.Tournament/Models/LadderEditorInfo.cs index 84ebeff3db..7b36096e3f 100644 --- a/osu.Game.Tournament/Models/LadderEditorInfo.cs +++ b/osu.Game.Tournament/Models/LadderEditorInfo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; namespace osu.Game.Tournament.Models diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs index cb4e8bc16a..b5bc5fd307 100644 --- a/osu.Game.Tournament/Models/LadderInfo.cs +++ b/osu.Game.Tournament/Models/LadderInfo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using Newtonsoft.Json; diff --git a/osu.Game.Tournament/Models/SeedingResult.cs b/osu.Game.Tournament/Models/SeedingResult.cs index 2a404153e6..865f65294e 100644 --- a/osu.Game.Tournament/Models/SeedingResult.cs +++ b/osu.Game.Tournament/Models/SeedingResult.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Bindables; diff --git a/osu.Game.Tournament/Models/TournamentProgression.cs b/osu.Game.Tournament/Models/TournamentProgression.cs index 6c3ba1922a..f119605026 100644 --- a/osu.Game.Tournament/Models/TournamentProgression.cs +++ b/osu.Game.Tournament/Models/TournamentProgression.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; namespace osu.Game.Tournament.Models diff --git a/osu.Game.Tournament/Models/TournamentRound.cs b/osu.Game.Tournament/Models/TournamentRound.cs index 480d6c37c3..a92bab690e 100644 --- a/osu.Game.Tournament/Models/TournamentRound.cs +++ b/osu.Game.Tournament/Models/TournamentRound.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using Newtonsoft.Json; diff --git a/osu.Game.Tournament/Properties/AssemblyInfo.cs b/osu.Game.Tournament/Properties/AssemblyInfo.cs index 2eb8c3e1d6..70e42bcafb 100644 --- a/osu.Game.Tournament/Properties/AssemblyInfo.cs +++ b/osu.Game.Tournament/Properties/AssemblyInfo.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Runtime.CompilerServices; // We publish our internal attributes to other sub-projects of the framework. diff --git a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs index 6f7234b8c3..e977012803 100644 --- a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs +++ b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.cs b/osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.cs index ac1d599851..1a2f5a1ff4 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Configuration; using osu.Framework.Platform; diff --git a/osu.Game.Tournament/Screens/Drawings/Components/Group.cs b/osu.Game.Tournament/Screens/Drawings/Components/Group.cs index b397f807f0..a79e2253a4 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/Group.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/Group.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/osu.Game.Tournament/Screens/Drawings/Components/GroupContainer.cs b/osu.Game.Tournament/Screens/Drawings/Components/GroupContainer.cs index 37e15b7e45..0a2fa3f207 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/GroupContainer.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/GroupContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs b/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs index 167a576424..137003a126 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; diff --git a/osu.Game.Tournament/Screens/Drawings/Components/ITeamList.cs b/osu.Game.Tournament/Screens/Drawings/Components/ITeamList.cs index 7e0ac89c83..09208818a9 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/ITeamList.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/ITeamList.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Game.Tournament.Models; diff --git a/osu.Game.Tournament/Screens/Editors/IModelBacked.cs b/osu.Game.Tournament/Screens/Editors/IModelBacked.cs index ca59afa2cb..867055f9ea 100644 --- a/osu.Game.Tournament/Screens/Editors/IModelBacked.cs +++ b/osu.Game.Tournament/Screens/Editors/IModelBacked.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Tournament.Screens.Editors { /// diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs index 75131c282d..4a3b5c0846 100644 --- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -29,7 +27,7 @@ namespace osu.Game.Tournament.Screens.Editors public TournamentRound Model { get; } [Resolved] - private LadderInfo ladderInfo { get; set; } + private LadderInfo ladderInfo { get; set; } = null!; public RoundRow(TournamentRound round) { @@ -146,7 +144,7 @@ namespace osu.Game.Tournament.Screens.Editors public RoundBeatmap Model { get; } [Resolved] - protected IAPIProvider API { get; private set; } + protected IAPIProvider API { get; private set; } = null!; private readonly Bindable beatmapId = new Bindable(); diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index a4358b4396..9927dd56a0 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -140,7 +138,7 @@ namespace osu.Game.Tournament.Screens.Editors public SeedingBeatmap Model { get; } [Resolved] - protected IAPIProvider API { get; private set; } + protected IAPIProvider API { get; private set; } = null!; private readonly Bindable beatmapId = new Bindable(); diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index c9d897ca11..93fbfba0e3 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -63,11 +61,11 @@ namespace osu.Game.Tournament.Screens.Editors private readonly Container drawableContainer; - [Resolved(canBeNull: true)] - private TournamentSceneManager sceneManager { get; set; } + [Resolved] + private TournamentSceneManager? sceneManager { get; set; } [Resolved] - private LadderInfo ladderInfo { get; set; } + private LadderInfo ladderInfo { get; set; } = null!; public TeamRow(TournamentTeam team, TournamentScreen parent) { @@ -211,10 +209,10 @@ namespace osu.Game.Tournament.Screens.Editors private readonly TournamentUser user; [Resolved] - protected IAPIProvider API { get; private set; } + protected IAPIProvider API { get; private set; } = null!; [Resolved] - private TournamentGameBase game { get; set; } + private TournamentGameBase game { get; set; } = null!; private readonly Bindable playerId = new Bindable(); diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs index d2b61220f0..79de4e465e 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Tournament.Components; diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index 60d1678326..c23327a43f 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs index 8b3786fa1f..c154e4fcef 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; diff --git a/osu.Game.Tournament/Screens/Ladder/Components/ProgressionPath.cs b/osu.Game.Tournament/Screens/Ladder/Components/ProgressionPath.cs index c79dbc26be..3d59bd8349 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/ProgressionPath.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/ProgressionPath.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Lines; diff --git a/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs index f7a42e4f50..00e5353edd 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; diff --git a/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs b/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs index 10d58612f4..80b46007e7 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs index 35d63f4fcf..db4d6198e6 100644 --- a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs +++ b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs b/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs index d04059118f..5d9ab5fa8f 100644 --- a/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs +++ b/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tournament/Screens/TournamentScreen.cs b/osu.Game.Tournament/Screens/TournamentScreen.cs index 02903a637c..1e119e0336 100644 --- a/osu.Game.Tournament/Screens/TournamentScreen.cs +++ b/osu.Game.Tournament/Screens/TournamentScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,7 +13,7 @@ namespace osu.Game.Tournament.Screens public const double FADE_DELAY = 200; [Resolved] - protected LadderInfo LadderInfo { get; private set; } + protected LadderInfo LadderInfo { get; private set; } = null!; protected TournamentScreen() { diff --git a/osu.Game.Tournament/TournamentSpriteText.cs b/osu.Game.Tournament/TournamentSpriteText.cs index 7ecb31ff15..227231a310 100644 --- a/osu.Game.Tournament/TournamentSpriteText.cs +++ b/osu.Game.Tournament/TournamentSpriteText.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Graphics; using osu.Game.Graphics.Sprites; diff --git a/osu.Game.Tournament/TourneyButton.cs b/osu.Game.Tournament/TourneyButton.cs index 558bd476c3..67426c1d58 100644 --- a/osu.Game.Tournament/TourneyButton.cs +++ b/osu.Game.Tournament/TourneyButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game.Tournament/WarningBox.cs b/osu.Game.Tournament/WarningBox.cs index 4a196446f6..a79e97914b 100644 --- a/osu.Game.Tournament/WarningBox.cs +++ b/osu.Game.Tournament/WarningBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game/Beatmaps/APIFailTimes.cs b/osu.Game/Beatmaps/APIFailTimes.cs index 441d30d06b..7218906b38 100644 --- a/osu.Game/Beatmaps/APIFailTimes.cs +++ b/osu.Game/Beatmaps/APIFailTimes.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using Newtonsoft.Json; diff --git a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs index fe4e815e62..be96a66614 100644 --- a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Localisation; diff --git a/osu.Game/Beatmaps/BeatmapOnlineStatus.cs b/osu.Game/Beatmaps/BeatmapOnlineStatus.cs index cdd99d4432..41393a8a39 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineStatus.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineStatus.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; using osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Beatmaps/BeatmapSetHypeStatus.cs b/osu.Game/Beatmaps/BeatmapSetHypeStatus.cs index b2d4cac210..4dd08203fc 100644 --- a/osu.Game/Beatmaps/BeatmapSetHypeStatus.cs +++ b/osu.Game/Beatmaps/BeatmapSetHypeStatus.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Newtonsoft.Json; namespace osu.Game.Beatmaps diff --git a/osu.Game/Beatmaps/BeatmapSetNominationStatus.cs b/osu.Game/Beatmaps/BeatmapSetNominationStatus.cs index cea0063814..8cf43ab320 100644 --- a/osu.Game/Beatmaps/BeatmapSetNominationStatus.cs +++ b/osu.Game/Beatmaps/BeatmapSetNominationStatus.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Newtonsoft.Json; namespace osu.Game.Beatmaps diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs b/osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs index 12424b797c..8d519158b6 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs +++ b/osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Newtonsoft.Json; namespace osu.Game.Beatmaps diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs b/osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs index ad2e994d3e..e727e2c37f 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs +++ b/osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Beatmaps { public struct BeatmapSetOnlineGenre diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs b/osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs index c71c279086..5656fab721 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs +++ b/osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Beatmaps { public struct BeatmapSetOnlineLanguage diff --git a/osu.Game/Beatmaps/BeatmapStatisticIcon.cs b/osu.Game/Beatmaps/BeatmapStatisticIcon.cs index ca07e5f365..b2d59646ae 100644 --- a/osu.Game/Beatmaps/BeatmapStatisticIcon.cs +++ b/osu.Game/Beatmaps/BeatmapStatisticIcon.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; diff --git a/osu.Game/Beatmaps/CountdownType.cs b/osu.Game/Beatmaps/CountdownType.cs index 7fb3de74fb..bbe9b648f7 100644 --- a/osu.Game/Beatmaps/CountdownType.cs +++ b/osu.Game/Beatmaps/CountdownType.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; namespace osu.Game.Beatmaps diff --git a/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs index 5b9cf6846c..052fae160f 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs index 84445dc14c..5d0e3891c9 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDownloadProgressBar.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDownloadProgressBar.cs index 3737715a7d..5ea42fe4b1 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDownloadProgressBar.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDownloadProgressBar.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -30,10 +28,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards private readonly Box foregroundFill; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [Resolved] - private OverlayColourProvider colourProvider { get; set; } + private OverlayColourProvider colourProvider { get; set; } = null!; public BeatmapCardDownloadProgressBar() { diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs index c99d1f0c76..ad91615031 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapSetFavouriteState.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapSetFavouriteState.cs index 8f8a47c199..4645ca822c 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapSetFavouriteState.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapSetFavouriteState.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Beatmaps.Drawables.Cards.Buttons; using osu.Game.Beatmaps.Drawables.Cards.Statistics; diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs index ee45d56b6e..e78fd651fe 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; diff --git a/osu.Game/Beatmaps/Drawables/Cards/ExpandedContentScrollContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/ExpandedContentScrollContainer.cs index 9a2a37a09a..b4298e493a 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/ExpandedContentScrollContainer.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/ExpandedContentScrollContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics; using osu.Framework.Input.Events; diff --git a/osu.Game/Beatmaps/Drawables/Cards/IconPill.cs b/osu.Game/Beatmaps/Drawables/Cards/IconPill.cs index 3cabbba98d..16be57ac95 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/IconPill.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/IconPill.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; diff --git a/osu.Game/Beatmaps/Drawables/Cards/Statistics/FavouritesStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/FavouritesStatistic.cs index 439e6acd22..6a329011f3 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Statistics/FavouritesStatistic.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/FavouritesStatistic.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Humanizer; using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; diff --git a/osu.Game/Beatmaps/Drawables/Cards/Statistics/PlayCountStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/PlayCountStatistic.cs index 45ab6ddb40..4ce37b8659 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Statistics/PlayCountStatistic.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/PlayCountStatistic.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Humanizer; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Beatmaps/Drawables/Cards/StoryboardIconPill.cs b/osu.Game/Beatmaps/Drawables/Cards/StoryboardIconPill.cs index 6de16da2b1..6cb19696f8 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/StoryboardIconPill.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/StoryboardIconPill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Beatmaps/Drawables/Cards/VideoIconPill.cs b/osu.Game/Beatmaps/Drawables/Cards/VideoIconPill.cs index 63b5e95b12..b96ed23826 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/VideoIconPill.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/VideoIconPill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs index efce0f80f1..2fb3a8eee4 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; diff --git a/osu.Game/Beatmaps/Drawables/DownloadProgressBar.cs b/osu.Game/Beatmaps/Drawables/DownloadProgressBar.cs index 9877b628db..55af57a45f 100644 --- a/osu.Game/Beatmaps/Drawables/DownloadProgressBar.cs +++ b/osu.Game/Beatmaps/Drawables/DownloadProgressBar.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; diff --git a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs index 36fff1dc3c..df8953d57c 100644 --- a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -47,10 +45,10 @@ namespace osu.Game.Beatmaps.Drawables public IBindable DisplayedStars => displayedStars; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; - [Resolved(canBeNull: true)] - private OverlayColourProvider colourProvider { get; set; } + [Resolved] + private OverlayColourProvider? colourProvider { get; set; } /// /// Creates a new using an already computed . diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs index 2cd9785048..72f37143d0 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -21,7 +19,7 @@ namespace osu.Game.Beatmaps.Drawables protected override double LoadDelay => 500; [Resolved] - private BeatmapManager beatmaps { get; set; } + private BeatmapManager beatmaps { get; set; } = null!; private readonly BeatmapSetCoverType beatmapSetCoverType; diff --git a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs index 0b53278ab3..d20baf1edb 100644 --- a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.IO; using osu.Framework.Audio.Track; diff --git a/osu.Game/Beatmaps/Formats/IHasCustomColours.cs b/osu.Game/Beatmaps/Formats/IHasCustomColours.cs index b651ef9515..9f2eeff253 100644 --- a/osu.Game/Beatmaps/Formats/IHasCustomColours.cs +++ b/osu.Game/Beatmaps/Formats/IHasCustomColours.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osuTK.Graphics; diff --git a/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs index 4f292a9a1f..a758e10f7c 100644 --- a/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.IO; using osu.Game.IO.Serialization; diff --git a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs index bf69100361..b3815569ec 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; namespace osu.Game.Beatmaps.Formats diff --git a/osu.Game/Beatmaps/Formats/Parsing.cs b/osu.Game/Beatmaps/Formats/Parsing.cs index 9b0d200077..a1683ced0d 100644 --- a/osu.Game/Beatmaps/Formats/Parsing.cs +++ b/osu.Game/Beatmaps/Formats/Parsing.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Globalization; diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 9dc3084cb5..fbe1a9b462 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps.ControlPoints; diff --git a/osu.Game/Beatmaps/IBeatmapConverter.cs b/osu.Game/Beatmaps/IBeatmapConverter.cs index f84188c5e2..2833af8ca2 100644 --- a/osu.Game/Beatmaps/IBeatmapConverter.cs +++ b/osu.Game/Beatmaps/IBeatmapConverter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Threading; diff --git a/osu.Game/Beatmaps/IBeatmapProcessor.cs b/osu.Game/Beatmaps/IBeatmapProcessor.cs index 0a4a98c606..014dccf5e3 100644 --- a/osu.Game/Beatmaps/IBeatmapProcessor.cs +++ b/osu.Game/Beatmaps/IBeatmapProcessor.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; diff --git a/osu.Game/Beatmaps/IBeatmapResourceProvider.cs b/osu.Game/Beatmaps/IBeatmapResourceProvider.cs index 9e79e03785..fe9dada9d5 100644 --- a/osu.Game/Beatmaps/IBeatmapResourceProvider.cs +++ b/osu.Game/Beatmaps/IBeatmapResourceProvider.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Game.IO; diff --git a/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs b/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs index 5c3c72c9e4..fc80f0db6f 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using JetBrains.Annotations; using Newtonsoft.Json; diff --git a/osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs b/osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs index b3717c81dc..78368daf09 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; namespace osu.Game.Beatmaps.Legacy diff --git a/osu.Game/Beatmaps/Legacy/LegacyEventType.cs b/osu.Game/Beatmaps/Legacy/LegacyEventType.cs index 42d21d14f6..c4a9566110 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyEventType.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyEventType.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Beatmaps.Legacy { internal enum LegacyEventType diff --git a/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs b/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs index ec947c6dc2..07f170f996 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; namespace osu.Game.Beatmaps.Legacy diff --git a/osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs b/osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs index d1782ec862..808a85b621 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; namespace osu.Game.Beatmaps.Legacy diff --git a/osu.Game/Beatmaps/Legacy/LegacyMods.cs b/osu.Game/Beatmaps/Legacy/LegacyMods.cs index 27b2e313ec..0e517ea3df 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyMods.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyMods.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; namespace osu.Game.Beatmaps.Legacy diff --git a/osu.Game/Beatmaps/Legacy/LegacySampleBank.cs b/osu.Game/Beatmaps/Legacy/LegacySampleBank.cs index f8a57c3ac9..70eb941a73 100644 --- a/osu.Game/Beatmaps/Legacy/LegacySampleBank.cs +++ b/osu.Game/Beatmaps/Legacy/LegacySampleBank.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Beatmaps.Legacy { internal enum LegacySampleBank diff --git a/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs b/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs index 69d0c96b57..5352fb65ed 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Beatmaps.Legacy { internal enum LegacyStoryLayer diff --git a/osu.Game/Beatmaps/Timing/BreakPeriod.cs b/osu.Game/Beatmaps/Timing/BreakPeriod.cs index 274b56a862..4c90b16745 100644 --- a/osu.Game/Beatmaps/Timing/BreakPeriod.cs +++ b/osu.Game/Beatmaps/Timing/BreakPeriod.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Screens.Play; namespace osu.Game.Beatmaps.Timing diff --git a/osu.Game/Configuration/BackgroundSource.cs b/osu.Game/Configuration/BackgroundSource.cs index f08b29cffe..2d9ed6df2c 100644 --- a/osu.Game/Configuration/BackgroundSource.cs +++ b/osu.Game/Configuration/BackgroundSource.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Configuration/DevelopmentOsuConfigManager.cs b/osu.Game/Configuration/DevelopmentOsuConfigManager.cs index 33329002a9..c979ebf453 100644 --- a/osu.Game/Configuration/DevelopmentOsuConfigManager.cs +++ b/osu.Game/Configuration/DevelopmentOsuConfigManager.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Platform; namespace osu.Game.Configuration diff --git a/osu.Game/Configuration/DiscordRichPresenceMode.cs b/osu.Game/Configuration/DiscordRichPresenceMode.cs index 150d23447e..6bec4fcc74 100644 --- a/osu.Game/Configuration/DiscordRichPresenceMode.cs +++ b/osu.Game/Configuration/DiscordRichPresenceMode.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Configuration/HUDVisibilityMode.cs b/osu.Game/Configuration/HUDVisibilityMode.cs index 9c69f33220..5ba7cbbe99 100644 --- a/osu.Game/Configuration/HUDVisibilityMode.cs +++ b/osu.Game/Configuration/HUDVisibilityMode.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Configuration/InMemoryConfigManager.cs b/osu.Game/Configuration/InMemoryConfigManager.cs index d8879daa3f..ccf697f680 100644 --- a/osu.Game/Configuration/InMemoryConfigManager.cs +++ b/osu.Game/Configuration/InMemoryConfigManager.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Configuration; diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 193068193a..309a5818ae 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Diagnostics; using osu.Framework.Bindables; diff --git a/osu.Game/Configuration/RandomSelectAlgorithm.cs b/osu.Game/Configuration/RandomSelectAlgorithm.cs index 052c6b4c55..985b4a8c55 100644 --- a/osu.Game/Configuration/RandomSelectAlgorithm.cs +++ b/osu.Game/Configuration/RandomSelectAlgorithm.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Configuration/ScalingMode.cs b/osu.Game/Configuration/ScalingMode.cs index e0ad59746b..3ad46d771c 100644 --- a/osu.Game/Configuration/ScalingMode.cs +++ b/osu.Game/Configuration/ScalingMode.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Configuration/ScreenshotFormat.cs b/osu.Game/Configuration/ScreenshotFormat.cs index 13d0b64fd2..f043781c45 100644 --- a/osu.Game/Configuration/ScreenshotFormat.cs +++ b/osu.Game/Configuration/ScreenshotFormat.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Configuration/ScrollVisualisationMethod.cs b/osu.Game/Configuration/ScrollVisualisationMethod.cs index 111bb95e67..5f48fe8bfd 100644 --- a/osu.Game/Configuration/ScrollVisualisationMethod.cs +++ b/osu.Game/Configuration/ScrollVisualisationMethod.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; namespace osu.Game.Configuration diff --git a/osu.Game/Configuration/SeasonalBackgroundMode.cs b/osu.Game/Configuration/SeasonalBackgroundMode.cs index 3e6d9e42aa..4ef71ef09c 100644 --- a/osu.Game/Configuration/SeasonalBackgroundMode.cs +++ b/osu.Game/Configuration/SeasonalBackgroundMode.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Configuration/SettingsStore.cs b/osu.Game/Configuration/SettingsStore.cs index fda7193fea..e5d2d572c8 100644 --- a/osu.Game/Configuration/SettingsStore.cs +++ b/osu.Game/Configuration/SettingsStore.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Database; namespace osu.Game.Configuration diff --git a/osu.Game/Configuration/StorageConfigManager.cs b/osu.Game/Configuration/StorageConfigManager.cs index c8781918e1..40c0e70488 100644 --- a/osu.Game/Configuration/StorageConfigManager.cs +++ b/osu.Game/Configuration/StorageConfigManager.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Configuration; using osu.Framework.Platform; diff --git a/osu.Game/Database/ICanAcceptFiles.cs b/osu.Game/Database/ICanAcceptFiles.cs index da970a29d4..bdbcebeaf5 100644 --- a/osu.Game/Database/ICanAcceptFiles.cs +++ b/osu.Game/Database/ICanAcceptFiles.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Threading.Tasks; diff --git a/osu.Game/Database/IHasFiles.cs b/osu.Game/Database/IHasFiles.cs index 9f8ce05218..3f6531832f 100644 --- a/osu.Game/Database/IHasFiles.cs +++ b/osu.Game/Database/IHasFiles.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using JetBrains.Annotations; diff --git a/osu.Game/Database/IHasGuidPrimaryKey.cs b/osu.Game/Database/IHasGuidPrimaryKey.cs index 9cf7cf0683..8520707bd2 100644 --- a/osu.Game/Database/IHasGuidPrimaryKey.cs +++ b/osu.Game/Database/IHasGuidPrimaryKey.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using Newtonsoft.Json; using Realms; diff --git a/osu.Game/Database/IHasNamedFiles.cs b/osu.Game/Database/IHasNamedFiles.cs index 3524eb4c99..34f6560f75 100644 --- a/osu.Game/Database/IHasNamedFiles.cs +++ b/osu.Game/Database/IHasNamedFiles.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; namespace osu.Game.Database diff --git a/osu.Game/Database/IHasPrimaryKey.cs b/osu.Game/Database/IHasPrimaryKey.cs index 84709ccd26..51a49948fe 100644 --- a/osu.Game/Database/IHasPrimaryKey.cs +++ b/osu.Game/Database/IHasPrimaryKey.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; diff --git a/osu.Game/Database/IModelFileManager.cs b/osu.Game/Database/IModelFileManager.cs index c40b57f663..390be4a69d 100644 --- a/osu.Game/Database/IModelFileManager.cs +++ b/osu.Game/Database/IModelFileManager.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.IO; namespace osu.Game.Database diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index 988178818d..ce79aac966 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; namespace osu.Game.Database diff --git a/osu.Game/Database/INamedFileInfo.cs b/osu.Game/Database/INamedFileInfo.cs index 9df4a0869c..d95f228440 100644 --- a/osu.Game/Database/INamedFileInfo.cs +++ b/osu.Game/Database/INamedFileInfo.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.IO; namespace osu.Game.Database diff --git a/osu.Game/Database/IPostNotifications.cs b/osu.Game/Database/IPostNotifications.cs index 8bb2c54945..205350b80c 100644 --- a/osu.Game/Database/IPostNotifications.cs +++ b/osu.Game/Database/IPostNotifications.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Game.Overlays.Notifications; diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs index 3ace67f410..beb7c5a4df 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; diff --git a/osu.Game/Graphics/Backgrounds/SkinBackground.cs b/osu.Game/Graphics/Backgrounds/SkinBackground.cs index e30bb961a0..17af5e6991 100644 --- a/osu.Game/Graphics/Backgrounds/SkinBackground.cs +++ b/osu.Game/Graphics/Backgrounds/SkinBackground.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Skinning; diff --git a/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs b/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs index 55160e14af..7722374c69 100644 --- a/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs +++ b/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs b/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs index a06af61125..5abb4096ac 100644 --- a/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs +++ b/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Graphics.Containers { /// diff --git a/osu.Game/Graphics/Containers/IExpandable.cs b/osu.Game/Graphics/Containers/IExpandable.cs index 05d569775c..8549d3bf2c 100644 --- a/osu.Game/Graphics/Containers/IExpandable.cs +++ b/osu.Game/Graphics/Containers/IExpandable.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/Containers/IExpandingContainer.cs b/osu.Game/Graphics/Containers/IExpandingContainer.cs index dbd9274ae7..6a36372e88 100644 --- a/osu.Game/Graphics/Containers/IExpandingContainer.cs +++ b/osu.Game/Graphics/Containers/IExpandingContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs index 5b1780a068..4a61ee2043 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Markdig; using Markdig.Extensions.Footnotes; using Markdig.Extensions.Tables; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownFencedCodeBlock.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownFencedCodeBlock.cs index b5bbe3e2cc..7d84d368ad 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownFencedCodeBlock.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownFencedCodeBlock.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Markdig.Syntax; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs index 800a0e1fc3..da4273c9b9 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Markdig.Syntax; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers.Markdown; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs index 8ccac158eb..10207dd389 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Markdig.Syntax.Inlines; using osu.Framework.Graphics.Containers.Markdown; using osu.Framework.Graphics.Cursor; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownOrderedListItem.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownOrderedListItem.cs index 6eac9378ae..1812a9529c 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownOrderedListItem.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownOrderedListItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownQuoteBlock.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownQuoteBlock.cs index 447085a48c..d2e6f4d370 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownQuoteBlock.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownQuoteBlock.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Markdig.Syntax; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownSeparator.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownSeparator.cs index 343a1d1015..b785b748a7 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownSeparator.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownSeparator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers.Markdown; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTable.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTable.cs index c9c1098e05..652d3d9e3e 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTable.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTable.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Markdig.Extensions.Tables; using osu.Framework.Graphics.Containers.Markdown; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTableCell.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTableCell.cs index dbf15a2546..4ffdeb179c 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTableCell.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTableCell.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Markdig.Extensions.Tables; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownUnorderedListItem.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownUnorderedListItem.cs index 64e98511c2..cdd6bc6ed5 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownUnorderedListItem.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownUnorderedListItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Graphics/Containers/OsuHoverContainer.cs b/osu.Game/Graphics/Containers/OsuHoverContainer.cs index b4b80f7574..3b5e48d23e 100644 --- a/osu.Game/Graphics/Containers/OsuHoverContainer.cs +++ b/osu.Game/Graphics/Containers/OsuHoverContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs b/osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs index e37d23fe97..892edf8551 100644 --- a/osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs +++ b/osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Graphics/Containers/ShakeContainer.cs b/osu.Game/Graphics/Containers/ShakeContainer.cs index 9a1ddac40d..bb9be2b939 100644 --- a/osu.Game/Graphics/Containers/ShakeContainer.cs +++ b/osu.Game/Graphics/Containers/ShakeContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics.Containers; using osu.Game.Extensions; diff --git a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs index 715677aec1..6934c95385 100644 --- a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs +++ b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; namespace osu.Game.Graphics.Containers diff --git a/osu.Game/Graphics/Containers/WaveContainer.cs b/osu.Game/Graphics/Containers/WaveContainer.cs index 05a666721a..9fd3d103c9 100644 --- a/osu.Game/Graphics/Containers/WaveContainer.cs +++ b/osu.Game/Graphics/Containers/WaveContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs index 27700e71d9..c5bcfcd2df 100644 --- a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs index dc75d626b9..aab5b3ee36 100644 --- a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; diff --git a/osu.Game/Graphics/DateTooltip.cs b/osu.Game/Graphics/DateTooltip.cs index c62f53f1d4..c084f498fe 100644 --- a/osu.Game/Graphics/DateTooltip.cs +++ b/osu.Game/Graphics/DateTooltip.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index 553b27acb1..0e5bcc8019 100644 --- a/osu.Game/Graphics/DrawableDate.cs +++ b/osu.Game/Graphics/DrawableDate.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/ErrorTextFlowContainer.cs b/osu.Game/Graphics/ErrorTextFlowContainer.cs index 65a90534e5..7386baf83f 100644 --- a/osu.Game/Graphics/ErrorTextFlowContainer.cs +++ b/osu.Game/Graphics/ErrorTextFlowContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; diff --git a/osu.Game/Graphics/IHasAccentColour.cs b/osu.Game/Graphics/IHasAccentColour.cs index fc722375ce..af497da70f 100644 --- a/osu.Game/Graphics/IHasAccentColour.cs +++ b/osu.Game/Graphics/IHasAccentColour.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; diff --git a/osu.Game/Graphics/OpenGL/Vertices/PositionAndColourVertex.cs b/osu.Game/Graphics/OpenGL/Vertices/PositionAndColourVertex.cs index 78c8cbb79e..2428eebbe5 100644 --- a/osu.Game/Graphics/OpenGL/Vertices/PositionAndColourVertex.cs +++ b/osu.Game/Graphics/OpenGL/Vertices/PositionAndColourVertex.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Runtime.InteropServices; using osu.Framework.Graphics.Rendering.Vertices; diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index e06f6b3fd0..1b21f79c0a 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Colour; diff --git a/osu.Game/Graphics/OsuIcon.cs b/osu.Game/Graphics/OsuIcon.cs index 0a099f1fcc..1f4d6a62da 100644 --- a/osu.Game/Graphics/OsuIcon.cs +++ b/osu.Game/Graphics/OsuIcon.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics.Sprites; namespace osu.Game.Graphics diff --git a/osu.Game/Graphics/ParticleExplosion.cs b/osu.Game/Graphics/ParticleExplosion.cs index 56e1568441..e16913c6c9 100644 --- a/osu.Game/Graphics/ParticleExplosion.cs +++ b/osu.Game/Graphics/ParticleExplosion.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs index ae594ddfe2..1355bfc272 100644 --- a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs +++ b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; diff --git a/osu.Game/Graphics/Sprites/OsuSpriteText.cs b/osu.Game/Graphics/Sprites/OsuSpriteText.cs index afbec0eab4..6ce64a9052 100644 --- a/osu.Game/Graphics/Sprites/OsuSpriteText.cs +++ b/osu.Game/Graphics/Sprites/OsuSpriteText.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Graphics/UserInterface/Bar.cs b/osu.Game/Graphics/UserInterface/Bar.cs index 53217e2120..9b63cabeda 100644 --- a/osu.Game/Graphics/UserInterface/Bar.cs +++ b/osu.Game/Graphics/UserInterface/Bar.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osuTK; using osuTK.Graphics; diff --git a/osu.Game/Graphics/UserInterface/BarGraph.cs b/osu.Game/Graphics/UserInterface/BarGraph.cs index c394e58d87..27a41eb7e3 100644 --- a/osu.Game/Graphics/UserInterface/BarGraph.cs +++ b/osu.Game/Graphics/UserInterface/BarGraph.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK; using osu.Framework.Graphics; using System.Collections.Generic; diff --git a/osu.Game/Graphics/UserInterface/BasicSearchTextBox.cs b/osu.Game/Graphics/UserInterface/BasicSearchTextBox.cs index c4e03133dc..2e76951e7b 100644 --- a/osu.Game/Graphics/UserInterface/BasicSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/BasicSearchTextBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osuTK; diff --git a/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs b/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs index ba76a17fc6..e0a5409683 100644 --- a/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs +++ b/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs b/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs index 5855a66ae1..b237dd9b71 100644 --- a/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs +++ b/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Graphics.UserInterfaceV2; diff --git a/osu.Game/Graphics/UserInterface/DialogButton.cs b/osu.Game/Graphics/UserInterface/DialogButton.cs index 670778b07b..06ebe48850 100644 --- a/osu.Game/Graphics/UserInterface/DialogButton.cs +++ b/osu.Game/Graphics/UserInterface/DialogButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework; using osu.Framework.Extensions.Color4Extensions; diff --git a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs index ec3a5744f8..5af275c9e7 100644 --- a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Graphics/UserInterface/ExpandableSlider.cs b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs index 5bc17303d8..121a1eef49 100644 --- a/osu.Game/Graphics/UserInterface/ExpandableSlider.cs +++ b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -106,8 +104,8 @@ namespace osu.Game.Graphics.UserInterface }; } - [Resolved(canBeNull: true)] - private IExpandingContainer expandingContainer { get; set; } + [Resolved] + private IExpandingContainer? expandingContainer { get; set; } protected override void LoadComplete() { diff --git a/osu.Game/Graphics/UserInterface/ExpandingBar.cs b/osu.Game/Graphics/UserInterface/ExpandingBar.cs index 6d7c41ee7c..bec4e6e49c 100644 --- a/osu.Game/Graphics/UserInterface/ExpandingBar.cs +++ b/osu.Game/Graphics/UserInterface/ExpandingBar.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osuTK; diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs index f0ff76b35d..72d50eb042 100644 --- a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs +++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; namespace osu.Game.Graphics.UserInterface diff --git a/osu.Game/Graphics/UserInterface/IconButton.cs b/osu.Game/Graphics/UserInterface/IconButton.cs index 47f06715b5..0268fa59c2 100644 --- a/osu.Game/Graphics/UserInterface/IconButton.cs +++ b/osu.Game/Graphics/UserInterface/IconButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK; using osuTK.Graphics; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/UserInterface/LoadingButton.cs b/osu.Game/Graphics/UserInterface/LoadingButton.cs index 8a841ffc94..4a37811114 100644 --- a/osu.Game/Graphics/UserInterface/LoadingButton.cs +++ b/osu.Game/Graphics/UserInterface/LoadingButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs index 0ea44dfe49..df921c5c81 100644 --- a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs +++ b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs index 69e8df0286..de4df96942 100644 --- a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -41,7 +39,7 @@ namespace osu.Game.Graphics.UserInterface } [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; protected override Container Content => content; diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs index 1b5f7cc4b5..96797e5d01 100644 --- a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -17,7 +15,7 @@ namespace osu.Game.Graphics.UserInterface private const int fade_duration = 250; [Resolved] - private OsuContextMenuSamples samples { get; set; } + private OsuContextMenuSamples samples { get; set; } = null!; // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed. private bool wasOpened; diff --git a/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs b/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs index dc089e3410..14f8f6f3c5 100644 --- a/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; namespace osu.Game.Graphics.UserInterface diff --git a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs index f6a3abdaae..df92863797 100644 --- a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Extensions; namespace osu.Game.Graphics.UserInterface diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs index 9de9eceb07..123854a2dd 100644 --- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -39,7 +37,7 @@ namespace osu.Game.Graphics.UserInterface private readonly CapsWarning warning; [Resolved] - private GameHost host { get; set; } + private GameHost host { get; set; } = null!; public OsuPasswordTextBox() { diff --git a/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs b/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs index 01d072b6d7..6272f95510 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK; using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; diff --git a/osu.Game/Graphics/UserInterface/PageSelector/PageEllipsis.cs b/osu.Game/Graphics/UserInterface/PageSelector/PageEllipsis.cs index 068e477d79..0f3b09f2dc 100644 --- a/osu.Game/Graphics/UserInterface/PageSelector/PageEllipsis.cs +++ b/osu.Game/Graphics/UserInterface/PageSelector/PageEllipsis.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Graphics/UserInterface/PageSelector/PageSelector.cs b/osu.Game/Graphics/UserInterface/PageSelector/PageSelector.cs index 63f35d5f89..416cd4b8ff 100644 --- a/osu.Game/Graphics/UserInterface/PageSelector/PageSelector.cs +++ b/osu.Game/Graphics/UserInterface/PageSelector/PageSelector.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/UserInterface/PercentageCounter.cs b/osu.Game/Graphics/UserInterface/PercentageCounter.cs index de93d9b2b4..ed06211e8f 100644 --- a/osu.Game/Graphics/UserInterface/PercentageCounter.cs +++ b/osu.Game/Graphics/UserInterface/PercentageCounter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Graphics/UserInterface/SearchTextBox.cs b/osu.Game/Graphics/UserInterface/SearchTextBox.cs index 2d09a239bb..a2e0ab6482 100644 --- a/osu.Game/Graphics/UserInterface/SearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SearchTextBox.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Graphics/UserInterface/SeekLimitedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/SeekLimitedSearchTextBox.cs index a85cd36808..11cf88c20e 100644 --- a/osu.Game/Graphics/UserInterface/SeekLimitedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SeekLimitedSearchTextBox.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Graphics.UserInterface { /// diff --git a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs index fb0a66cb8d..3940bf8bca 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs b/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs index e3f5bc65e6..33382305cf 100644 --- a/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs +++ b/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Graphics/UserInterface/StarCounter.cs b/osu.Game/Graphics/UserInterface/StarCounter.cs index d7d088d798..82f6f8e0a4 100644 --- a/osu.Game/Graphics/UserInterface/StarCounter.cs +++ b/osu.Game/Graphics/UserInterface/StarCounter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Graphics/UserInterface/TimeSlider.cs b/osu.Game/Graphics/UserInterface/TimeSlider.cs index e6e7ae9305..7652eda7be 100644 --- a/osu.Game/Graphics/UserInterface/TimeSlider.cs +++ b/osu.Game/Graphics/UserInterface/TimeSlider.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; namespace osu.Game.Graphics.UserInterface diff --git a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs index aa542b8f49..5532e5c6a7 100644 --- a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs +++ b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledColourPalette.cs index 721d8990ba..163d1fb2b9 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledColourPalette.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledColourPalette.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs index 8fd9a62ad7..59a1812e5d 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledDropdown.cs index 0e2ea362da..dbbae390a7 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledDropdown.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledDropdown.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledEnumDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledEnumDropdown.cs index 3ca460be90..9c2c8397ed 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledEnumDropdown.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledEnumDropdown.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledNumberBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledNumberBox.cs index 2643db0547..4926afd7a3 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledNumberBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledNumberBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Graphics.UserInterface; namespace osu.Game.Graphics.UserInterfaceV2 diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs index 00f4ef1a30..4585d3a4c9 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics; using osu.Game.Overlays.Settings; diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledSwitchButton.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledSwitchButton.cs index 3c27829de3..d25bcbfbe9 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledSwitchButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledSwitchButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Graphics.UserInterfaceV2 { public partial class LabelledSwitchButton : LabelledComponent diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuColourPicker.cs b/osu.Game/Graphics/UserInterfaceV2/OsuColourPicker.cs index fed17eaf20..cf86206f5c 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuColourPicker.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuColourPicker.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics.UserInterface; namespace osu.Game.Graphics.UserInterfaceV2 diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs index beaeb86243..c0ac9f21ca 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.IO; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs b/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs index ff51f3aa92..75ff1e5665 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs b/osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs index 9aa650d88d..6633ba0eb2 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs index d89322cecd..9153d5253d 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Extensions; diff --git a/osu.Game/IO/Archives/LegacyByteArrayReader.cs b/osu.Game/IO/Archives/LegacyByteArrayReader.cs index e58dbb48ce..06db02b335 100644 --- a/osu.Game/IO/Archives/LegacyByteArrayReader.cs +++ b/osu.Game/IO/Archives/LegacyByteArrayReader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.IO; diff --git a/osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs b/osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs index e26f6af081..dfae58aed7 100644 --- a/osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs +++ b/osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.IO; using System.Linq; diff --git a/osu.Game/IO/Archives/LegacyFileArchiveReader.cs b/osu.Game/IO/Archives/LegacyFileArchiveReader.cs index aee1add2f6..c317cae5fc 100644 --- a/osu.Game/IO/Archives/LegacyFileArchiveReader.cs +++ b/osu.Game/IO/Archives/LegacyFileArchiveReader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.IO; diff --git a/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs b/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs index d47f936eb3..8d14385707 100644 --- a/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs +++ b/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.IO; diff --git a/osu.Game/IO/Legacy/ILegacySerializable.cs b/osu.Game/IO/Legacy/ILegacySerializable.cs index f21e67a34b..e5518a0a30 100644 --- a/osu.Game/IO/Legacy/ILegacySerializable.cs +++ b/osu.Game/IO/Legacy/ILegacySerializable.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.IO.Legacy { public interface ILegacySerializable diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index f4c55e4b0e..a2b89b6d97 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Diagnostics; using System.Linq; using JetBrains.Annotations; diff --git a/osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.cs b/osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.cs index 65283d0d82..450eebd736 100644 --- a/osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.cs +++ b/osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; diff --git a/osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.cs b/osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.cs index b51a8473ca..d01e8de26d 100644 --- a/osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.cs +++ b/osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Newtonsoft.Json.Serialization; using osu.Game.Extensions; diff --git a/osu.Game/Input/IdleTracker.cs b/osu.Game/Input/IdleTracker.cs index 54157c9e3d..40f270c234 100644 --- a/osu.Game/Input/IdleTracker.cs +++ b/osu.Game/Input/IdleTracker.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -68,7 +66,7 @@ namespace osu.Game.Input public void OnReleased(KeyBindingReleaseEvent e) => updateLastInteractionTime(); [Resolved] - private GameHost host { get; set; } + private GameHost host { get; set; } = null!; protected override bool Handle(UIEvent e) { diff --git a/osu.Game/Input/OsuConfineMouseMode.cs b/osu.Game/Input/OsuConfineMouseMode.cs index 2d914ac6e0..e875ceebd9 100644 --- a/osu.Game/Input/OsuConfineMouseMode.cs +++ b/osu.Game/Input/OsuConfineMouseMode.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Input; using osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Input/OsuUserInputManager.cs b/osu.Game/Input/OsuUserInputManager.cs index c205636ab9..b8fd0bb11c 100644 --- a/osu.Game/Input/OsuUserInputManager.cs +++ b/osu.Game/Input/OsuUserInputManager.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Input; using osuTK.Input; diff --git a/osu.Game/Online/API/APIException.cs b/osu.Game/Online/API/APIException.cs index 21a9c761c7..7491e375df 100644 --- a/osu.Game/Online/API/APIException.cs +++ b/osu.Game/Online/API/APIException.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; namespace osu.Game.Online.API diff --git a/osu.Game/Online/API/APIMessagesRequest.cs b/osu.Game/Online/API/APIMessagesRequest.cs index 5bb3e29621..3ad6b1d7c8 100644 --- a/osu.Game/Online/API/APIMessagesRequest.cs +++ b/osu.Game/Online/API/APIMessagesRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.IO.Network; using osu.Game.Online.Chat; diff --git a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs index df64984c7a..8df2d3fc2d 100644 --- a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs +++ b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Buffers; using System.Collections.Generic; using System.Text; diff --git a/osu.Game/Online/API/OsuJsonWebRequest.cs b/osu.Game/Online/API/OsuJsonWebRequest.cs index 2d402edd3f..eb0be57d9f 100644 --- a/osu.Game/Online/API/OsuJsonWebRequest.cs +++ b/osu.Game/Online/API/OsuJsonWebRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.IO.Network; namespace osu.Game.Online.API diff --git a/osu.Game/Online/API/OsuWebRequest.cs b/osu.Game/Online/API/OsuWebRequest.cs index 9a7cf45a2f..ee7115fa96 100644 --- a/osu.Game/Online/API/OsuWebRequest.cs +++ b/osu.Game/Online/API/OsuWebRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.IO.Network; namespace osu.Game.Online.API diff --git a/osu.Game/Online/API/Requests/CommentVoteRequest.cs b/osu.Game/Online/API/Requests/CommentVoteRequest.cs index a835b0365c..06a3b1126e 100644 --- a/osu.Game/Online/API/Requests/CommentVoteRequest.cs +++ b/osu.Game/Online/API/Requests/CommentVoteRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.IO.Network; using osu.Game.Online.API.Requests.Responses; using System.Net.Http; diff --git a/osu.Game/Online/API/Requests/CreateChannelRequest.cs b/osu.Game/Online/API/Requests/CreateChannelRequest.cs index 130210b1c3..e660c4a883 100644 --- a/osu.Game/Online/API/Requests/CreateChannelRequest.cs +++ b/osu.Game/Online/API/Requests/CreateChannelRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using System.Net.Http; using osu.Framework.IO.Network; diff --git a/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs b/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs index 6b7192dbf4..0429a30b0b 100644 --- a/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs +++ b/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Online/API/Requests/Cursor.cs b/osu.Game/Online/API/Requests/Cursor.cs index c7bb119bd8..8ddbce39ca 100644 --- a/osu.Game/Online/API/Requests/Cursor.cs +++ b/osu.Game/Online/API/Requests/Cursor.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using JetBrains.Annotations; using Newtonsoft.Json; diff --git a/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs index f190b6e821..5254dc3cf8 100644 --- a/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs +++ b/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.IO.Network; using osu.Game.Beatmaps; diff --git a/osu.Game/Online/API/Requests/DownloadReplayRequest.cs b/osu.Game/Online/API/Requests/DownloadReplayRequest.cs index 5635c4728e..77174f0bb5 100644 --- a/osu.Game/Online/API/Requests/DownloadReplayRequest.cs +++ b/osu.Game/Online/API/Requests/DownloadReplayRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Scoring; namespace osu.Game.Online.API.Requests diff --git a/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs index f3690c934d..158ae03b8d 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests diff --git a/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs index e118d1bddc..6cb9e6dd44 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; diff --git a/osu.Game/Online/API/Requests/GetChangelogBuildRequest.cs b/osu.Game/Online/API/Requests/GetChangelogBuildRequest.cs index 2d2d241b86..baa15c70c4 100644 --- a/osu.Game/Online/API/Requests/GetChangelogBuildRequest.cs +++ b/osu.Game/Online/API/Requests/GetChangelogBuildRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests diff --git a/osu.Game/Online/API/Requests/GetChangelogRequest.cs b/osu.Game/Online/API/Requests/GetChangelogRequest.cs index 82ed42615f..97799ff66a 100644 --- a/osu.Game/Online/API/Requests/GetChangelogRequest.cs +++ b/osu.Game/Online/API/Requests/GetChangelogRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests diff --git a/osu.Game/Online/API/Requests/GetCommentsRequest.cs b/osu.Game/Online/API/Requests/GetCommentsRequest.cs index 1aa08f2ed8..1e033a58a7 100644 --- a/osu.Game/Online/API/Requests/GetCommentsRequest.cs +++ b/osu.Game/Online/API/Requests/GetCommentsRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.IO.Network; using osu.Game.Extensions; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Online/API/Requests/GetCountryRankingsRequest.cs b/osu.Game/Online/API/Requests/GetCountryRankingsRequest.cs index 9d037ab116..d8a1198627 100644 --- a/osu.Game/Online/API/Requests/GetCountryRankingsRequest.cs +++ b/osu.Game/Online/API/Requests/GetCountryRankingsRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets; namespace osu.Game.Online.API.Requests diff --git a/osu.Game/Online/API/Requests/GetFriendsRequest.cs b/osu.Game/Online/API/Requests/GetFriendsRequest.cs index 640ddcbb9e..63a221d91a 100644 --- a/osu.Game/Online/API/Requests/GetFriendsRequest.cs +++ b/osu.Game/Online/API/Requests/GetFriendsRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Online/API/Requests/GetMessagesRequest.cs b/osu.Game/Online/API/Requests/GetMessagesRequest.cs index 2f9879c63f..651f8a06c5 100644 --- a/osu.Game/Online/API/Requests/GetMessagesRequest.cs +++ b/osu.Game/Online/API/Requests/GetMessagesRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Game.Online.Chat; diff --git a/osu.Game/Online/API/Requests/GetRankingsRequest.cs b/osu.Game/Online/API/Requests/GetRankingsRequest.cs index f42da69dcc..ddc3298ca7 100644 --- a/osu.Game/Online/API/Requests/GetRankingsRequest.cs +++ b/osu.Game/Online/API/Requests/GetRankingsRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.IO.Network; using osu.Game.Rulesets; diff --git a/osu.Game/Online/API/Requests/GetSeasonalBackgroundsRequest.cs b/osu.Game/Online/API/Requests/GetSeasonalBackgroundsRequest.cs index 0ecce90749..941b47244a 100644 --- a/osu.Game/Online/API/Requests/GetSeasonalBackgroundsRequest.cs +++ b/osu.Game/Online/API/Requests/GetSeasonalBackgroundsRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests diff --git a/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs b/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs index 2d8a8b3b61..59b2928a2a 100644 --- a/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs +++ b/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.IO.Network; using osu.Game.Overlays.Rankings; using osu.Game.Rulesets; diff --git a/osu.Game/Online/API/Requests/GetTopUsersRequest.cs b/osu.Game/Online/API/Requests/GetTopUsersRequest.cs index 7f05cd5eab..dbbd2119db 100644 --- a/osu.Game/Online/API/Requests/GetTopUsersRequest.cs +++ b/osu.Game/Online/API/Requests/GetTopUsersRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Online.API.Requests { public class GetTopUsersRequest : APIRequest diff --git a/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs index e4134980b1..226bc6bf1e 100644 --- a/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Game.Extensions; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs b/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs index 3d0ee23080..67d3ad26b0 100644 --- a/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.cs index e5e65415f7..bef3df42fb 100644 --- a/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs b/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs index c27a83b695..628fc1be96 100644 --- a/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.IO.Network; using osu.Game.Rulesets; using osu.Game.Users; diff --git a/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs b/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs index 82cf0a508a..79f0549d4a 100644 --- a/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Online/API/Requests/GetWikiRequest.cs b/osu.Game/Online/API/Requests/GetWikiRequest.cs index 7c84e1f790..f6bd80e210 100644 --- a/osu.Game/Online/API/Requests/GetWikiRequest.cs +++ b/osu.Game/Online/API/Requests/GetWikiRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Extensions; using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Online/API/Requests/JoinChannelRequest.cs b/osu.Game/Online/API/Requests/JoinChannelRequest.cs index 30b8fafd57..33eab7e355 100644 --- a/osu.Game/Online/API/Requests/JoinChannelRequest.cs +++ b/osu.Game/Online/API/Requests/JoinChannelRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.Chat; diff --git a/osu.Game/Online/API/Requests/LeaveChannelRequest.cs b/osu.Game/Online/API/Requests/LeaveChannelRequest.cs index 4e77055e67..7dfc9a0aed 100644 --- a/osu.Game/Online/API/Requests/LeaveChannelRequest.cs +++ b/osu.Game/Online/API/Requests/LeaveChannelRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.Chat; diff --git a/osu.Game/Online/API/Requests/ListChannelsRequest.cs b/osu.Game/Online/API/Requests/ListChannelsRequest.cs index 6f8fb427dc..9660695c14 100644 --- a/osu.Game/Online/API/Requests/ListChannelsRequest.cs +++ b/osu.Game/Online/API/Requests/ListChannelsRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Game.Online.Chat; diff --git a/osu.Game/Online/API/Requests/MarkChannelAsReadRequest.cs b/osu.Game/Online/API/Requests/MarkChannelAsReadRequest.cs index afdc8a47f4..b24669e6d5 100644 --- a/osu.Game/Online/API/Requests/MarkChannelAsReadRequest.cs +++ b/osu.Game/Online/API/Requests/MarkChannelAsReadRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.Chat; diff --git a/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs b/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs index 4af1f58180..44b2b17d66 100644 --- a/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs +++ b/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Globalization; using osu.Framework.IO.Network; diff --git a/osu.Game/Online/API/Requests/PaginationParameters.cs b/osu.Game/Online/API/Requests/PaginationParameters.cs index 6dacb009bd..cab56bcf2e 100644 --- a/osu.Game/Online/API/Requests/PaginationParameters.cs +++ b/osu.Game/Online/API/Requests/PaginationParameters.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Online.API.Requests { /// diff --git a/osu.Game/Online/API/Requests/PostBeatmapFavouriteRequest.cs b/osu.Game/Online/API/Requests/PostBeatmapFavouriteRequest.cs index 1438c9c436..9fdc3382aa 100644 --- a/osu.Game/Online/API/Requests/PostBeatmapFavouriteRequest.cs +++ b/osu.Game/Online/API/Requests/PostBeatmapFavouriteRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.IO.Network; using System.Net.Http; diff --git a/osu.Game/Online/API/Requests/PostMessageRequest.cs b/osu.Game/Online/API/Requests/PostMessageRequest.cs index e3709d8f13..64b88f0340 100644 --- a/osu.Game/Online/API/Requests/PostMessageRequest.cs +++ b/osu.Game/Online/API/Requests/PostMessageRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.Chat; diff --git a/osu.Game/Online/API/Requests/Responses/APIPlayStyle.cs b/osu.Game/Online/API/Requests/Responses/APIPlayStyle.cs index 4a877f392a..be43fc8ae7 100644 --- a/osu.Game/Online/API/Requests/Responses/APIPlayStyle.cs +++ b/osu.Game/Online/API/Requests/Responses/APIPlayStyle.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Online/API/Requests/Responses/APIUserAchievement.cs b/osu.Game/Online/API/Requests/Responses/APIUserAchievement.cs index 65001dd6cf..836a5bc485 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUserAchievement.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUserAchievement.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using Newtonsoft.Json; diff --git a/osu.Game/Online/API/Requests/Responses/APIUserHistoryCount.cs b/osu.Game/Online/API/Requests/Responses/APIUserHistoryCount.cs index 6eb3c8b8a4..9a34699a1b 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUserHistoryCount.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUserHistoryCount.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using Newtonsoft.Json; diff --git a/osu.Game/Online/Chat/DrawableLinkCompiler.cs b/osu.Game/Online/Chat/DrawableLinkCompiler.cs index ee53c00668..883a2496f7 100644 --- a/osu.Game/Online/Chat/DrawableLinkCompiler.cs +++ b/osu.Game/Online/Chat/DrawableLinkCompiler.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -27,8 +25,8 @@ namespace osu.Game.Online.Chat /// public readonly List Parts; - [Resolved(CanBeNull = true)] - private OverlayColourProvider overlayColourProvider { get; set; } + [Resolved] + private OverlayColourProvider? overlayColourProvider { get; set; } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parts.Any(d => d.ReceivePositionalInputAt(screenSpacePos)); diff --git a/osu.Game/Online/Chat/ErrorMessage.cs b/osu.Game/Online/Chat/ErrorMessage.cs index 9cd91a0927..ccfc0e29b4 100644 --- a/osu.Game/Online/Chat/ErrorMessage.cs +++ b/osu.Game/Online/Chat/ErrorMessage.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Online.Chat { public class ErrorMessage : InfoMessage diff --git a/osu.Game/Online/DevelopmentEndpointConfiguration.cs b/osu.Game/Online/DevelopmentEndpointConfiguration.cs index 3171d15fc2..69276f452a 100644 --- a/osu.Game/Online/DevelopmentEndpointConfiguration.cs +++ b/osu.Game/Online/DevelopmentEndpointConfiguration.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Online { public class DevelopmentEndpointConfiguration : EndpointConfiguration diff --git a/osu.Game/Online/Leaderboards/DrawableRank.cs b/osu.Game/Online/Leaderboards/DrawableRank.cs index 5a65c15444..5177f35478 100644 --- a/osu.Game/Online/Leaderboards/DrawableRank.cs +++ b/osu.Game/Online/Leaderboards/DrawableRank.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; diff --git a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs index 68bf3cfaec..f266c38b8b 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Threading.Tasks; namespace osu.Game.Online.Multiplayer diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index a2608f1564..b7a5faf7c9 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Threading.Tasks; using osu.Game.Online.API; diff --git a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs index 305c41e69b..d3da8f491b 100644 --- a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs +++ b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; diff --git a/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs b/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs index ab513e71ee..4c793dba68 100644 --- a/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs +++ b/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; diff --git a/osu.Game/Online/Multiplayer/InvalidStateException.cs b/osu.Game/Online/Multiplayer/InvalidStateException.cs index ba3b84ffe4..27b111a781 100644 --- a/osu.Game/Online/Multiplayer/InvalidStateException.cs +++ b/osu.Game/Online/Multiplayer/InvalidStateException.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; diff --git a/osu.Game/Online/Multiplayer/MatchUserRequest.cs b/osu.Game/Online/Multiplayer/MatchUserRequest.cs index 7fc1790434..8515256581 100644 --- a/osu.Game/Online/Multiplayer/MatchUserRequest.cs +++ b/osu.Game/Online/Multiplayer/MatchUserRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using MessagePack; using osu.Game.Online.Multiplayer.Countdown; diff --git a/osu.Game/Online/Multiplayer/NotHostException.cs b/osu.Game/Online/Multiplayer/NotHostException.cs index 9f789f1e81..cd43b13e52 100644 --- a/osu.Game/Online/Multiplayer/NotHostException.cs +++ b/osu.Game/Online/Multiplayer/NotHostException.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; diff --git a/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs b/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs index c749e4615a..0a96406c16 100644 --- a/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs +++ b/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; diff --git a/osu.Game/Online/Multiplayer/QueueMode.cs b/osu.Game/Online/Multiplayer/QueueMode.cs index a7bc4ae00a..adc975539f 100644 --- a/osu.Game/Online/Multiplayer/QueueMode.cs +++ b/osu.Game/Online/Multiplayer/QueueMode.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; namespace osu.Game.Online.Multiplayer diff --git a/osu.Game/Online/Placeholders/LoginPlaceholder.cs b/osu.Game/Online/Placeholders/LoginPlaceholder.cs index de7ac6e936..60c9ecbcdc 100644 --- a/osu.Game/Online/Placeholders/LoginPlaceholder.cs +++ b/osu.Game/Online/Placeholders/LoginPlaceholder.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; @@ -12,8 +10,8 @@ namespace osu.Game.Online.Placeholders { public sealed partial class LoginPlaceholder : ClickablePlaceholder { - [Resolved(CanBeNull = true)] - private LoginOverlay login { get; set; } + [Resolved] + private LoginOverlay? login { get; set; } public LoginPlaceholder(LocalisableString actionMessage) : base(actionMessage, FontAwesome.Solid.UserLock) diff --git a/osu.Game/Online/Placeholders/MessagePlaceholder.cs b/osu.Game/Online/Placeholders/MessagePlaceholder.cs index 07a111a10f..bef889a576 100644 --- a/osu.Game/Online/Placeholders/MessagePlaceholder.cs +++ b/osu.Game/Online/Placeholders/MessagePlaceholder.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; diff --git a/osu.Game/Online/Rooms/APIScoreToken.cs b/osu.Game/Online/Rooms/APIScoreToken.cs index 58a633f3cf..542f972d3f 100644 --- a/osu.Game/Online/Rooms/APIScoreToken.cs +++ b/osu.Game/Online/Rooms/APIScoreToken.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Newtonsoft.Json; namespace osu.Game.Online.Rooms diff --git a/osu.Game/Online/Rooms/CreateRoomRequest.cs b/osu.Game/Online/Rooms/CreateRoomRequest.cs index b22780490b..63a3b7bfa8 100644 --- a/osu.Game/Online/Rooms/CreateRoomRequest.cs +++ b/osu.Game/Online/Rooms/CreateRoomRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Net.Http; using Newtonsoft.Json; using osu.Framework.IO.Network; diff --git a/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs b/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs index 65731b2b68..c31c6a929a 100644 --- a/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.API; diff --git a/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs b/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs index 6b5ed2d024..e016074f5d 100644 --- a/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Online.API; namespace osu.Game.Online.Rooms diff --git a/osu.Game/Online/Rooms/GetRoomRequest.cs b/osu.Game/Online/Rooms/GetRoomRequest.cs index 237d427509..b968f4e864 100644 --- a/osu.Game/Online/Rooms/GetRoomRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Online.API; namespace osu.Game.Online.Rooms diff --git a/osu.Game/Online/Rooms/GetRoomsRequest.cs b/osu.Game/Online/Rooms/GetRoomsRequest.cs index afab83b5be..7feb709acb 100644 --- a/osu.Game/Online/Rooms/GetRoomsRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomsRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.IO.Network; using osu.Game.Extensions; diff --git a/osu.Game/Online/Rooms/IndexScoresParams.cs b/osu.Game/Online/Rooms/IndexScoresParams.cs index 253caa13a1..b69af27e8b 100644 --- a/osu.Game/Online/Rooms/IndexScoresParams.cs +++ b/osu.Game/Online/Rooms/IndexScoresParams.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using JetBrains.Annotations; using Newtonsoft.Json; diff --git a/osu.Game/Online/Rooms/ItemAttemptsCount.cs b/osu.Game/Online/Rooms/ItemAttemptsCount.cs index 71f50b9898..dc86897660 100644 --- a/osu.Game/Online/Rooms/ItemAttemptsCount.cs +++ b/osu.Game/Online/Rooms/ItemAttemptsCount.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Newtonsoft.Json; namespace osu.Game.Online.Rooms diff --git a/osu.Game/Online/Rooms/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs index 0a687312e7..a1d6ed1e82 100644 --- a/osu.Game/Online/Rooms/JoinRoomRequest.cs +++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.API; diff --git a/osu.Game/Online/Rooms/MatchType.cs b/osu.Game/Online/Rooms/MatchType.cs index fd2f583f83..28f2da897a 100644 --- a/osu.Game/Online/Rooms/MatchType.cs +++ b/osu.Game/Online/Rooms/MatchType.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Online/Rooms/MultiplayerScores.cs b/osu.Game/Online/Rooms/MultiplayerScores.cs index 4c13579b3e..3331a2155c 100644 --- a/osu.Game/Online/Rooms/MultiplayerScores.cs +++ b/osu.Game/Online/Rooms/MultiplayerScores.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Online.API.Requests; diff --git a/osu.Game/Online/Rooms/PartRoomRequest.cs b/osu.Game/Online/Rooms/PartRoomRequest.cs index da4e9a44c5..09ba6f65c3 100644 --- a/osu.Game/Online/Rooms/PartRoomRequest.cs +++ b/osu.Game/Online/Rooms/PartRoomRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.API; diff --git a/osu.Game/Online/Rooms/RoomAvailability.cs b/osu.Game/Online/Rooms/RoomAvailability.cs index fada111826..3aea0e5948 100644 --- a/osu.Game/Online/Rooms/RoomAvailability.cs +++ b/osu.Game/Online/Rooms/RoomAvailability.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; namespace osu.Game.Online.Rooms diff --git a/osu.Game/Online/Rooms/RoomCategory.cs b/osu.Game/Online/Rooms/RoomCategory.cs index ba17fb2121..17afb0dc7f 100644 --- a/osu.Game/Online/Rooms/RoomCategory.cs +++ b/osu.Game/Online/Rooms/RoomCategory.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; namespace osu.Game.Online.Rooms diff --git a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs index 9aa6424592..0fc27d26b8 100644 --- a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs +++ b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Graphics; using osuTK.Graphics; diff --git a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs index c37b93ea1b..5cc664cf36 100644 --- a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs +++ b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Graphics; using osuTK.Graphics; diff --git a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs index 9eb61a82ec..4d0c93b8ab 100644 --- a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs +++ b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Graphics; using osuTK.Graphics; diff --git a/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs b/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs index affb2846a2..8e6a1ac7c7 100644 --- a/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs +++ b/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Online.API; namespace osu.Game.Online.Rooms diff --git a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs index f4cadc3fde..3f404386c3 100644 --- a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Scoring; namespace osu.Game.Online.Rooms diff --git a/osu.Game/Online/Rooms/SubmitScoreRequest.cs b/osu.Game/Online/Rooms/SubmitScoreRequest.cs index 48a7780a03..b4a91a5892 100644 --- a/osu.Game/Online/Rooms/SubmitScoreRequest.cs +++ b/osu.Game/Online/Rooms/SubmitScoreRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Net.Http; using Newtonsoft.Json; using osu.Framework.IO.Network; diff --git a/osu.Game/Online/SignalRWorkaroundTypes.cs b/osu.Game/Online/SignalRWorkaroundTypes.cs index 0b545821ee..0e3eb0aab0 100644 --- a/osu.Game/Online/SignalRWorkaroundTypes.cs +++ b/osu.Game/Online/SignalRWorkaroundTypes.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using osu.Game.Online.Multiplayer; diff --git a/osu.Game/Online/Solo/CreateSoloScoreRequest.cs b/osu.Game/Online/Solo/CreateSoloScoreRequest.cs index 8c92b32915..2f462b2610 100644 --- a/osu.Game/Online/Solo/CreateSoloScoreRequest.cs +++ b/osu.Game/Online/Solo/CreateSoloScoreRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Globalization; using System.Net.Http; using osu.Framework.IO.Network; diff --git a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs index f387e61901..3260ba8e7c 100644 --- a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs +++ b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Online.Rooms; using osu.Game.Scoring; diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs index 605ebc4ef0..9605604966 100644 --- a/osu.Game/Online/Spectator/ISpectatorClient.cs +++ b/osu.Game/Online/Spectator/ISpectatorClient.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Threading.Tasks; namespace osu.Game.Online.Spectator diff --git a/osu.Game/Online/Spectator/ISpectatorServer.cs b/osu.Game/Online/Spectator/ISpectatorServer.cs index fa9d04792a..848983009b 100644 --- a/osu.Game/Online/Spectator/ISpectatorServer.cs +++ b/osu.Game/Online/Spectator/ISpectatorServer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Threading.Tasks; namespace osu.Game.Online.Spectator diff --git a/osu.Game/OsuGameBase_Importing.cs b/osu.Game/OsuGameBase_Importing.cs index cf65460bab..601d17bea9 100644 --- a/osu.Game/OsuGameBase_Importing.cs +++ b/osu.Game/OsuGameBase_Importing.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.IO; using System.Linq; diff --git a/osu.Game/Overlays/AccountCreation/AccountCreationBackground.cs b/osu.Game/Overlays/AccountCreation/AccountCreationBackground.cs index 0042b9f8f6..8d015709aa 100644 --- a/osu.Game/Overlays/AccountCreation/AccountCreationBackground.cs +++ b/osu.Game/Overlays/AccountCreation/AccountCreationBackground.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Overlays/AccountCreation/AccountCreationScreen.cs b/osu.Game/Overlays/AccountCreation/AccountCreationScreen.cs index a7dd53f511..32e5ca471c 100644 --- a/osu.Game/Overlays/AccountCreation/AccountCreationScreen.cs +++ b/osu.Game/Overlays/AccountCreation/AccountCreationScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Screens; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs index 2f290d05e9..e3e2bcaf9a 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs index 3ab0e47a6c..608ecbb6f3 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs index 96626d0ac6..2efa4b455e 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs index 031833a107..195ac09c3e 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions; diff --git a/osu.Game/Overlays/BeatmapListing/SearchCategory.cs b/osu.Game/Overlays/BeatmapListing/SearchCategory.cs index b3e12d00a6..2c77177541 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchCategory.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchCategory.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/BeatmapListing/SearchExplicit.cs b/osu.Game/Overlays/BeatmapListing/SearchExplicit.cs index 10fea6d5b2..4df5cd4b0a 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchExplicit.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchExplicit.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/BeatmapListing/SearchExtra.cs b/osu.Game/Overlays/BeatmapListing/SearchExtra.cs index d307cd09eb..e54632acd8 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchExtra.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchExtra.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs b/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs index ebcbef1ad9..28d8473a6e 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/BeatmapListing/SearchGenre.cs b/osu.Game/Overlays/BeatmapListing/SearchGenre.cs index 7746eb50d7..f928f8a7bb 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchGenre.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchGenre.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs index d52f923158..af8a298919 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs b/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs index 0c379b3825..3b04ac01ca 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/BeatmapListing/SortCriteria.cs b/osu.Game/Overlays/BeatmapListing/SortCriteria.cs index 6c010c7504..5e3cbbfeea 100644 --- a/osu.Game/Overlays/BeatmapListing/SortCriteria.cs +++ b/osu.Game/Overlays/BeatmapListing/SortCriteria.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs b/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs index 9291988367..7d8160bef7 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; using osu.Game.Rulesets; diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetLayoutSection.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetLayoutSection.cs index 305a3661a7..4dd257c6ea 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetLayoutSection.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetLayoutSection.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs index b3b8b80a0d..62a8bf80d3 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs index 8758b9c5cf..c182ef2e15 100644 --- a/osu.Game/Overlays/BeatmapSet/Info.cs +++ b/osu.Game/Overlays/BeatmapSet/Info.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs b/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs index 476a252c7b..5cfe4a35b3 100644 --- a/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs +++ b/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Screens.Select.Leaderboards; using osu.Game.Graphics.UserInterface; using osu.Framework.Allocation; diff --git a/osu.Game/Overlays/BeatmapSet/MetadataType.cs b/osu.Game/Overlays/BeatmapSet/MetadataType.cs index dc96ce99e9..c92cecc17e 100644 --- a/osu.Game/Overlays/BeatmapSet/MetadataType.cs +++ b/osu.Game/Overlays/BeatmapSet/MetadataType.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/NoScoresPlaceholder.cs b/osu.Game/Overlays/BeatmapSet/Scores/NoScoresPlaceholder.cs index f7703af27d..29a696593d 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/NoScoresPlaceholder.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/NoScoresPlaceholder.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Screens.Select.Leaderboards; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/NotSupporterPlaceholder.cs b/osu.Game/Overlays/BeatmapSet/Scores/NotSupporterPlaceholder.cs index 04ab3ec72f..7cb119bf32 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/NotSupporterPlaceholder.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/NotSupporterPlaceholder.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTableRowBackground.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTableRowBackground.cs index 130dfd45e7..8a6545a97b 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTableRowBackground.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTableRowBackground.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs index 04cbf171f6..59ba9cd449 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Game.Extensions; using osu.Game.Graphics; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs index afaed85250..9dc2ce204f 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; diff --git a/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs b/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs index e730496b5c..67b11af0c1 100644 --- a/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs +++ b/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Overlays/Changelog/ChangelogEntry.cs b/osu.Game/Overlays/Changelog/ChangelogEntry.cs index ab671d9c86..06ec2043f9 100644 --- a/osu.Game/Overlays/Changelog/ChangelogEntry.cs +++ b/osu.Game/Overlays/Changelog/ChangelogEntry.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Diagnostics; using System.Net; @@ -26,10 +24,10 @@ namespace osu.Game.Overlays.Changelog private readonly APIChangelogEntry entry; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [Resolved] - private OverlayColourProvider colourProvider { get; set; } + private OverlayColourProvider colourProvider { get; set; } = null!; private FontUsage fontLarge; private FontUsage fontMedium; diff --git a/osu.Game/Overlays/Changelog/ChangelogListing.cs b/osu.Game/Overlays/Changelog/ChangelogListing.cs index 4b784c7a28..3a648f66cf 100644 --- a/osu.Game/Overlays/Changelog/ChangelogListing.cs +++ b/osu.Game/Overlays/Changelog/ChangelogListing.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using osu.Framework.Allocation; diff --git a/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs b/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs index 4aded1dd59..012ccf8f64 100644 --- a/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs +++ b/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs index 155cbc7d65..e3dd989e2d 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Overlays.Changelog diff --git a/osu.Game/Overlays/Chat/ChannelScrollContainer.cs b/osu.Game/Overlays/Chat/ChannelScrollContainer.cs index 090f7835ae..6d8b21a7c5 100644 --- a/osu.Game/Overlays/Chat/ChannelScrollContainer.cs +++ b/osu.Game/Overlays/Chat/ChannelScrollContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Graphics.Containers; diff --git a/osu.Game/Overlays/Comments/Buttons/ChevronButton.cs b/osu.Game/Overlays/Comments/Buttons/ChevronButton.cs index 88e7d00476..45024f25db 100644 --- a/osu.Game/Overlays/Comments/Buttons/ChevronButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/ChevronButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Graphics.Containers; using osu.Framework.Bindables; diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs index d9576f5b72..400820ddd9 100644 --- a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -26,7 +24,7 @@ namespace osu.Game.Overlays.Comments.Buttons } [Resolved] - private OverlayColourProvider colourProvider { get; set; } + private OverlayColourProvider colourProvider { get; set; } = null!; private readonly ChevronIcon icon; private readonly Box background; diff --git a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs index 9cc20caa05..278cef9112 100644 --- a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs +++ b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Markdig.Syntax; using osu.Framework.Graphics.Containers.Markdown; using osu.Game.Graphics.Containers.Markdown; diff --git a/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs b/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs index 1770fcb269..50bd08b66b 100644 --- a/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs +++ b/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs b/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs index 6adb388185..fa366f38c3 100644 --- a/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs +++ b/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics.Containers; using osu.Framework.Graphics; using osu.Game.Graphics; diff --git a/osu.Game/Overlays/Comments/HeaderButton.cs b/osu.Game/Overlays/Comments/HeaderButton.cs index de99cd6cc8..1a26148e49 100644 --- a/osu.Game/Overlays/Comments/HeaderButton.cs +++ b/osu.Game/Overlays/Comments/HeaderButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs index 5cbeb8f306..0f4697e33c 100644 --- a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs +++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; using osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs b/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs index 3bb42ec953..4abece9a8d 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Overlays.Dashboard.Friends { public class FriendStream diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs index 785eef38ad..2aea631b7c 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Extensions; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs b/osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs index 21bc5b8203..dc291f1a44 100644 --- a/osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs +++ b/osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/Dashboard/Friends/UserListToolbar.cs b/osu.Game/Overlays/Dashboard/Friends/UserListToolbar.cs index db8510325c..3f31ceee1a 100644 --- a/osu.Game/Overlays/Dashboard/Friends/UserListToolbar.cs +++ b/osu.Game/Overlays/Dashboard/Friends/UserListToolbar.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osuTK; diff --git a/osu.Game/Overlays/Dashboard/Friends/UserSortTabControl.cs b/osu.Game/Overlays/Dashboard/Friends/UserSortTabControl.cs index 886ed08af2..db01e1f266 100644 --- a/osu.Game/Overlays/Dashboard/Friends/UserSortTabControl.cs +++ b/osu.Game/Overlays/Dashboard/Friends/UserSortTabControl.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapListing.cs b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapListing.cs index 0282ba8785..4eac1a1d29 100644 --- a/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapListing.cs +++ b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapListing.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Dashboard/Home/DashboardNewBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Home/DashboardNewBeatmapPanel.cs index fef33bdf5a..9e9c22fea2 100644 --- a/osu.Game/Overlays/Dashboard/Home/DashboardNewBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/DashboardNewBeatmapPanel.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics; using osu.Game.Graphics; diff --git a/osu.Game/Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.cs index 54d95c994b..a08a1fef6f 100644 --- a/osu.Game/Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Overlays/Dashboard/Home/DrawableBeatmapList.cs b/osu.Game/Overlays/Dashboard/Home/DrawableBeatmapList.cs index af36f71dd2..a0e22a0faf 100644 --- a/osu.Game/Overlays/Dashboard/Home/DrawableBeatmapList.cs +++ b/osu.Game/Overlays/Dashboard/Home/DrawableBeatmapList.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; diff --git a/osu.Game/Overlays/Dashboard/Home/DrawableNewBeatmapList.cs b/osu.Game/Overlays/Dashboard/Home/DrawableNewBeatmapList.cs index 8a60d8568c..a22ce8acb0 100644 --- a/osu.Game/Overlays/Dashboard/Home/DrawableNewBeatmapList.cs +++ b/osu.Game/Overlays/Dashboard/Home/DrawableNewBeatmapList.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Localisation; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Overlays/Dashboard/Home/DrawablePopularBeatmapList.cs b/osu.Game/Overlays/Dashboard/Home/DrawablePopularBeatmapList.cs index aab99d0ed3..b321057ef2 100644 --- a/osu.Game/Overlays/Dashboard/Home/DrawablePopularBeatmapList.cs +++ b/osu.Game/Overlays/Dashboard/Home/DrawablePopularBeatmapList.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Localisation; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Overlays/Dashboard/Home/HomePanel.cs b/osu.Game/Overlays/Dashboard/Home/HomePanel.cs index 8023c093aa..93db9978a7 100644 --- a/osu.Game/Overlays/Dashboard/Home/HomePanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/HomePanel.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs index dabe65964a..86babf82b5 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Extensions.LocalisationExtensions; diff --git a/osu.Game/Overlays/Dashboard/Home/News/NewsGroupItem.cs b/osu.Game/Overlays/Dashboard/Home/News/NewsGroupItem.cs index 9b27d1a193..9319d0dabd 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/NewsGroupItem.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/NewsGroupItem.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Extensions.LocalisationExtensions; diff --git a/osu.Game/Overlays/Dashboard/Home/News/NewsItemGroupPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/NewsItemGroupPanel.cs index fa59f38690..f0d51be52a 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/NewsItemGroupPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/NewsItemGroupPanel.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; diff --git a/osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs b/osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs index 1960e0372e..d6f8499c85 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 527ac1689b..2f96421531 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Game.Overlays.Dashboard; using osu.Game.Overlays.Dashboard.Friends; diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index f5a7e9e43d..9969677826 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions.Color4Extensions; diff --git a/osu.Game/Overlays/Dialog/PopupDialogButton.cs b/osu.Game/Overlays/Dialog/PopupDialogButton.cs index 91a19add21..499dab3a1d 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Extensions.Color4Extensions; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Overlays/Dialog/PopupDialogCancelButton.cs b/osu.Game/Overlays/Dialog/PopupDialogCancelButton.cs index f4289c66f1..c55226b147 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogCancelButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogCancelButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Overlays/Dialog/PopupDialogOkButton.cs b/osu.Game/Overlays/Dialog/PopupDialogOkButton.cs index eb4a0f0709..968657755f 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogOkButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogOkButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Overlays/FullscreenOverlay.cs b/osu.Game/Overlays/FullscreenOverlay.cs index 032821f215..b58a3b929b 100644 --- a/osu.Game/Overlays/FullscreenOverlay.cs +++ b/osu.Game/Overlays/FullscreenOverlay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -29,7 +27,7 @@ namespace osu.Game.Overlays protected virtual Color4 BackgroundColour => ColourProvider.Background5; [Resolved] - protected IAPIProvider API { get; private set; } + protected IAPIProvider API { get; private set; } = null!; [Cached] protected readonly OverlayColourProvider ColourProvider; diff --git a/osu.Game/Overlays/INamedOverlayComponent.cs b/osu.Game/Overlays/INamedOverlayComponent.cs index e9d01a55e3..65664b12e7 100644 --- a/osu.Game/Overlays/INamedOverlayComponent.cs +++ b/osu.Game/Overlays/INamedOverlayComponent.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; namespace osu.Game.Overlays diff --git a/osu.Game/Overlays/INotificationOverlay.cs b/osu.Game/Overlays/INotificationOverlay.cs index b9ac466229..93346cb0ee 100644 --- a/osu.Game/Overlays/INotificationOverlay.cs +++ b/osu.Game/Overlays/INotificationOverlay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Overlays.Notifications; diff --git a/osu.Game/Overlays/IOverlayManager.cs b/osu.Game/Overlays/IOverlayManager.cs index 0318b2b3a0..d771308e34 100644 --- a/osu.Game/Overlays/IOverlayManager.cs +++ b/osu.Game/Overlays/IOverlayManager.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Overlays/Login/UserDropdown.cs b/osu.Game/Overlays/Login/UserDropdown.cs index 0bdfa82517..78b5271ca0 100644 --- a/osu.Game/Overlays/Login/UserDropdown.cs +++ b/osu.Game/Overlays/Login/UserDropdown.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Mods/DeselectAllModsButton.cs b/osu.Game/Overlays/Mods/DeselectAllModsButton.cs index 817b6beac3..0e60fc3414 100644 --- a/osu.Game/Overlays/Mods/DeselectAllModsButton.cs +++ b/osu.Game/Overlays/Mods/DeselectAllModsButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; diff --git a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs index ee4f932326..9e16aa926f 100644 --- a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs +++ b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs index 93279b6e1c..26c5b2ac49 100644 --- a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs +++ b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -19,7 +17,7 @@ namespace osu.Game.Overlays.Mods private readonly BindableBool incompatible = new BindableBool(); [Resolved] - private Bindable> selectedMods { get; set; } + private Bindable> selectedMods { get; set; } = null!; public IncompatibilityDisplayingModPanel(ModState modState) : base(modState) diff --git a/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs b/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs index 1723634774..2f82711162 100644 --- a/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs +++ b/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -24,7 +22,7 @@ namespace osu.Game.Overlays.Mods private readonly Bindable> incompatibleMods = new Bindable>(); [Resolved] - private Bindable ruleset { get; set; } + private Bindable ruleset { get; set; } = null!; public IncompatibilityDisplayingTooltip() { diff --git a/osu.Game/Overlays/Mods/ModSettingsArea.cs b/osu.Game/Overlays/Mods/ModSettingsArea.cs index f11fef1299..fe1d683d59 100644 --- a/osu.Game/Overlays/Mods/ModSettingsArea.cs +++ b/osu.Game/Overlays/Mods/ModSettingsArea.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -32,7 +30,7 @@ namespace osu.Game.Overlays.Mods private readonly FillFlowContainer modSettingsFlow; [Resolved] - private OverlayColourProvider colourProvider { get; set; } + private OverlayColourProvider colourProvider { get; set; } = null!; public ModSettingsArea() { diff --git a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs index 827caf0467..b7d265c448 100644 --- a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs +++ b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; @@ -24,16 +22,16 @@ namespace osu.Game.Overlays.Music public partial class MusicKeyBindingHandler : Component, IKeyBindingHandler { [Resolved] - private IBindable beatmap { get; set; } + private IBindable beatmap { get; set; } = null!; [Resolved] - private MusicController musicController { get; set; } - - [Resolved(canBeNull: true)] - private OnScreenDisplay onScreenDisplay { get; set; } + private MusicController musicController { get; set; } = null!; [Resolved] - private OsuGame game { get; set; } + private OnScreenDisplay? onScreenDisplay { get; set; } + + [Resolved] + private OsuGame game { get; set; } = null!; public bool OnPressed(KeyBindingPressEvent e) { diff --git a/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs b/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs index ae59fbb35e..fa9a2e3972 100644 --- a/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs +++ b/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; diff --git a/osu.Game/Overlays/News/NewsPostBackground.cs b/osu.Game/Overlays/News/NewsPostBackground.cs index 05f8a639fa..663747255a 100644 --- a/osu.Game/Overlays/News/NewsPostBackground.cs +++ b/osu.Game/Overlays/News/NewsPostBackground.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs b/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs index 7d0d07fc1b..758eea93d4 100644 --- a/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs +++ b/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics.Sprites; namespace osu.Game.Overlays.Notifications diff --git a/osu.Game/Overlays/OSD/Toast.cs b/osu.Game/Overlays/OSD/Toast.cs index ff8696c04f..7df534d90d 100644 --- a/osu.Game/Overlays/OSD/Toast.cs +++ b/osu.Game/Overlays/OSD/Toast.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Overlays/OnlineOverlay.cs b/osu.Game/Overlays/OnlineOverlay.cs index 8b7a82f899..051873b394 100644 --- a/osu.Game/Overlays/OnlineOverlay.cs +++ b/osu.Game/Overlays/OnlineOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/OverlayColourProvider.cs b/osu.Game/Overlays/OverlayColourProvider.cs index d7581960f4..a4f6527024 100644 --- a/osu.Game/Overlays/OverlayColourProvider.cs +++ b/osu.Game/Overlays/OverlayColourProvider.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osuTK; using osuTK.Graphics; diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs index 93de463204..827a7749af 100644 --- a/osu.Game/Overlays/OverlayHeader.cs +++ b/osu.Game/Overlays/OverlayHeader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/OverlayHeaderBackground.cs b/osu.Game/Overlays/OverlayHeaderBackground.cs index a089001385..7e264ee196 100644 --- a/osu.Game/Overlays/OverlayHeaderBackground.cs +++ b/osu.Game/Overlays/OverlayHeaderBackground.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Overlays/OverlayRulesetSelector.cs b/osu.Game/Overlays/OverlayRulesetSelector.cs index 9205a14d9f..e10bcda734 100644 --- a/osu.Game/Overlays/OverlayRulesetSelector.cs +++ b/osu.Game/Overlays/OverlayRulesetSelector.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Overlays/OverlayRulesetTabItem.cs b/osu.Game/Overlays/OverlayRulesetTabItem.cs index d5c70a46d0..6d318820b3 100644 --- a/osu.Game/Overlays/OverlayRulesetTabItem.cs +++ b/osu.Game/Overlays/OverlayRulesetTabItem.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -35,7 +33,7 @@ namespace osu.Game.Overlays protected override Container Content { get; } [Resolved] - private OverlayColourProvider colourProvider { get; set; } + private OverlayColourProvider colourProvider { get; set; } = null!; private readonly Drawable icon; diff --git a/osu.Game/Overlays/OverlaySidebar.cs b/osu.Game/Overlays/OverlaySidebar.cs index 93e5e83ffc..b08a9d08a4 100644 --- a/osu.Game/Overlays/OverlaySidebar.cs +++ b/osu.Game/Overlays/OverlaySidebar.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Rankings/CountryFilter.cs b/osu.Game/Overlays/Rankings/CountryFilter.cs index 525816f8fd..c4e281402b 100644 --- a/osu.Game/Overlays/Rankings/CountryFilter.cs +++ b/osu.Game/Overlays/Rankings/CountryFilter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Rankings/CountryPill.cs b/osu.Game/Overlays/Rankings/CountryPill.cs index 5efa9d12f0..294b6df34d 100644 --- a/osu.Game/Overlays/Rankings/CountryPill.cs +++ b/osu.Game/Overlays/Rankings/CountryPill.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Overlays/Rankings/RankingsScope.cs b/osu.Game/Overlays/Rankings/RankingsScope.cs index 2644fee58b..3392db9360 100644 --- a/osu.Game/Overlays/Rankings/RankingsScope.cs +++ b/osu.Game/Overlays/Rankings/RankingsScope.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/Rankings/RankingsSortTabControl.cs b/osu.Game/Overlays/Rankings/RankingsSortTabControl.cs index 9e73c3adb0..b13ecc190e 100644 --- a/osu.Game/Overlays/Rankings/RankingsSortTabControl.cs +++ b/osu.Game/Overlays/Rankings/RankingsSortTabControl.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs b/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs index 3be5cc994c..fb3e58d2ac 100644 --- a/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using System; @@ -68,8 +66,8 @@ namespace osu.Game.Overlays.Rankings.Tables private partial class CountryName : LinkFlowContainer { - [Resolved(canBeNull: true)] - private RankingsOverlay rankings { get; set; } + [Resolved] + private RankingsOverlay? rankings { get; set; } public CountryName(CountryCode countryCode) : base(t => t.Font = OsuFont.GetFont(size: 12)) diff --git a/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs b/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs index 19ed3afdca..f13fbd66ec 100644 --- a/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Rankings/Tables/ScoresTable.cs b/osu.Game/Overlays/Rankings/Tables/ScoresTable.cs index 0da3fba8cc..9005334dda 100644 --- a/osu.Game/Overlays/Rankings/Tables/ScoresTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/ScoresTable.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Rankings/Tables/TableRowBackground.cs b/osu.Game/Overlays/Rankings/Tables/TableRowBackground.cs index 54ec45f4ff..8c50e72207 100644 --- a/osu.Game/Overlays/Rankings/Tables/TableRowBackground.cs +++ b/osu.Game/Overlays/Rankings/Tables/TableRowBackground.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Overlays/Settings/DangerousSettingsButton.cs b/osu.Game/Overlays/Settings/DangerousSettingsButton.cs index 248b4d339a..42b042ae75 100644 --- a/osu.Game/Overlays/Settings/DangerousSettingsButton.cs +++ b/osu.Game/Overlays/Settings/DangerousSettingsButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Graphics; diff --git a/osu.Game/Overlays/Settings/ISettingsItem.cs b/osu.Game/Overlays/Settings/ISettingsItem.cs index 509fc1ab0d..b77a8d9268 100644 --- a/osu.Game/Overlays/Settings/ISettingsItem.cs +++ b/osu.Game/Overlays/Settings/ISettingsItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Settings/OutlinedTextBox.cs b/osu.Game/Overlays/Settings/OutlinedTextBox.cs index 56b662ecf0..ef2039f8bf 100644 --- a/osu.Game/Overlays/Settings/OutlinedTextBox.cs +++ b/osu.Game/Overlays/Settings/OutlinedTextBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Input.Events; diff --git a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs index fc354027c1..6b5c769853 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; diff --git a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs index 7066be4f92..2bb5fa983f 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Settings/Sections/AudioSection.cs b/osu.Game/Overlays/Settings/Sections/AudioSection.cs index 542d5bc8fd..fb3d486776 100644 --- a/osu.Game/Overlays/Settings/Sections/AudioSection.cs +++ b/osu.Game/Overlays/Settings/Sections/AudioSection.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/DebugSection.cs b/osu.Game/Overlays/Settings/Sections/DebugSection.cs index 509410fbb1..33a6f4c673 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSection.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSection.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs index 6c2bfedba0..1044810bdc 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs index bf0a48d2c2..0d2d163859 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Threading; using System.Threading.Tasks; diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs index 00eb6fa62c..467c988020 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs index 09e5f3e163..048351b4cb 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs index da5fc519e6..9efdfa9955 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 96d458a942..c7f5aa5665 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs index c67c14bb43..e7c83159cd 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs index 9291dfe923..c245a1a9ea 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs index f6b3c12487..79a971510f 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index ae6145752b..b60689b611 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs index 8054b27de5..c7180ec51b 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs index 323cdaf14d..98f6908512 100644 --- a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs index 2b478f6af3..a93e6c37af 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingPanel.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingPanel.cs index 30429c84f0..7296003c7f 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingPanel.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingPanel.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Input.Bindings; diff --git a/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs b/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs index 19f0d0f7d1..0ed7de9d3f 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs index 0647068da7..a8f19cc91d 100644 --- a/osu.Game/Overlays/Settings/Sections/InputSection.cs +++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectoryLocationDialog.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectoryLocationDialog.cs index 633bf8c5a5..fcbc603c83 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectoryLocationDialog.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectoryLocationDialog.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; @@ -15,7 +13,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance public partial class StableDirectoryLocationDialog : PopupDialog { [Resolved] - private IPerformFromScreenRunner performer { get; set; } + private IPerformFromScreenRunner performer { get; set; } = null!; public StableDirectoryLocationDialog(TaskCompletionSource taskCompletionSource) { diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.cs index 048f3ee683..1f62077f20 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.IO; using System.Linq; using System.Threading.Tasks; diff --git a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs index dc6743c042..e7b6aa56a8 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/Online/IntegrationSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/IntegrationSettings.cs index 33748d0f5e..3d0fac32cf 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/IntegrationSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/IntegrationSettings.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs index d0707a434a..d34b01ebf3 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs index 775c6f9839..c8faa3b697 100644 --- a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs +++ b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/SizeSlider.cs b/osu.Game/Overlays/Settings/Sections/SizeSlider.cs index 20d77bef0d..c73831d8d1 100644 --- a/osu.Game/Overlays/Settings/Sections/SizeSlider.cs +++ b/osu.Game/Overlays/Settings/Sections/SizeSlider.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Globalization; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs index 2e8d005401..844b8aeac6 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs index d3303e409c..addf5ce163 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs b/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs index 0926574a54..2ec9e32ea9 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/SettingsCheckbox.cs b/osu.Game/Overlays/Settings/SettingsCheckbox.cs index a413bcf220..f8edcaf53d 100644 --- a/osu.Game/Overlays/Settings/SettingsCheckbox.cs +++ b/osu.Game/Overlays/Settings/SettingsCheckbox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs index 62dd4f2905..cf6bc30f85 100644 --- a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Overlays/Settings/SettingsFooter.cs b/osu.Game/Overlays/Settings/SettingsFooter.cs index 9ab0fa7ad8..ffb955f3bd 100644 --- a/osu.Game/Overlays/Settings/SettingsFooter.cs +++ b/osu.Game/Overlays/Settings/SettingsFooter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Graphics; @@ -83,7 +81,7 @@ namespace osu.Game.Overlays.Settings private readonly string version; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; public BuildDisplay(string version) { diff --git a/osu.Game/Overlays/Settings/SettingsHeader.cs b/osu.Game/Overlays/Settings/SettingsHeader.cs index f2b84c4ba9..8d155fd01e 100644 --- a/osu.Game/Overlays/Settings/SettingsHeader.cs +++ b/osu.Game/Overlays/Settings/SettingsHeader.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index 97b8f6de60..7912890528 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Settings/SettingsSidebar.cs b/osu.Game/Overlays/Settings/SettingsSidebar.cs index 36411e01cc..06bc2fd788 100644 --- a/osu.Game/Overlays/Settings/SettingsSidebar.cs +++ b/osu.Game/Overlays/Settings/SettingsSidebar.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game/Overlays/Settings/SettingsSlider.cs b/osu.Game/Overlays/Settings/SettingsSlider.cs index e1483d4202..6c81fece13 100644 --- a/osu.Game/Overlays/Settings/SettingsSlider.cs +++ b/osu.Game/Overlays/Settings/SettingsSlider.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Overlays/Settings/SettingsSubsection.cs b/osu.Game/Overlays/Settings/SettingsSubsection.cs index eda18abaef..87772eb18c 100644 --- a/osu.Game/Overlays/Settings/SettingsSubsection.cs +++ b/osu.Game/Overlays/Settings/SettingsSubsection.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Overlays/Settings/SettingsTextBox.cs b/osu.Game/Overlays/Settings/SettingsTextBox.cs index 3f9fa06384..7fc7e1a97b 100644 --- a/osu.Game/Overlays/Settings/SettingsTextBox.cs +++ b/osu.Game/Overlays/Settings/SettingsTextBox.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs index aec0509394..5fe0e1afc0 100644 --- a/osu.Game/Overlays/Settings/SidebarButton.cs +++ b/osu.Game/Overlays/Settings/SidebarButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; @@ -14,7 +12,7 @@ namespace osu.Game.Overlays.Settings protected const double FADE_DURATION = 500; [Resolved] - protected OverlayColourProvider ColourProvider { get; private set; } + protected OverlayColourProvider ColourProvider { get; private set; } = null!; protected SidebarButton() : base(HoverSampleSet.ButtonSidebar) diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs index 2b87535708..edfe38b2da 100644 --- a/osu.Game/Overlays/TabControlOverlayHeader.cs +++ b/osu.Game/Overlays/TabControlOverlayHeader.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Overlays/Toolbar/ClockDisplay.cs b/osu.Game/Overlays/Toolbar/ClockDisplay.cs index 088631f8d6..c72c92b61b 100644 --- a/osu.Game/Overlays/Toolbar/ClockDisplay.cs +++ b/osu.Game/Overlays/Toolbar/ClockDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs index efcb011293..247be553e1 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Input.Bindings; diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 4193e52584..e181322dda 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -39,10 +37,10 @@ namespace osu.Game.Overlays.Toolbar } [Resolved] - private TextureStore textures { get; set; } + private TextureStore textures { get; set; } = null!; [Resolved] - private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } + private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!; public void SetIcon(string texture) => SetIcon(new Sprite @@ -81,7 +79,7 @@ namespace osu.Game.Overlays.Toolbar protected FillFlowContainer Flow; [Resolved] - private RealmAccess realm { get; set; } + private RealmAccess realm { get; set; } = null!; protected ToolbarButton() { diff --git a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs index 30e32d831c..06f171b1f2 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs index 7bb94067ab..126f8383ce 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Input.Bindings; diff --git a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs index dba4e8feb6..ba2c8282c5 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Input.Bindings; using osu.Game.Localisation; diff --git a/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs index bdcf6c3fec..13900dffa9 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs index 3dfec2cba0..1871371750 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs index ddbf4889b6..3e94ff90c5 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs index 07f7d52545..7d75acb9d1 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs index 6ebf2a4c02..78df060252 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Input.Bindings; diff --git a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs index a8a88813d2..8e6a5fdb78 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Input.Bindings; diff --git a/osu.Game/Overlays/Toolbar/ToolbarWikiButton.cs b/osu.Game/Overlays/Toolbar/ToolbarWikiButton.cs index 49e6be7978..e60aea53c3 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarWikiButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarWikiButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/VersionManager.cs b/osu.Game/Overlays/VersionManager.cs index 0e74cada29..71f8fc05aa 100644 --- a/osu.Game/Overlays/VersionManager.cs +++ b/osu.Game/Overlays/VersionManager.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Volume/MuteButton.cs b/osu.Game/Overlays/Volume/MuteButton.cs index c83ad4ac0d..1dc8d754b7 100644 --- a/osu.Game/Overlays/Volume/MuteButton.cs +++ b/osu.Game/Overlays/Volume/MuteButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs index 34fbec93b7..153f7f5412 100644 --- a/osu.Game/Overlays/WaveOverlayContainer.cs +++ b/osu.Game/Overlays/WaveOverlayContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics.Containers; using osu.Framework.Graphics; using osu.Game.Graphics.Containers; diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs index 7c36caa62f..9107ad342b 100644 --- a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs +++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using Markdig.Extensions.CustomContainers; using Markdig.Extensions.Yaml; diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImage.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImage.cs index 71c2df538d..cfeb4de19c 100644 --- a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImage.cs +++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImage.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Markdig.Syntax.Inlines; using osu.Game.Graphics.Containers.Markdown; diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs index 641c6242b6..bb7c232a13 100644 --- a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs +++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Markdig.Syntax.Inlines; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -16,7 +14,7 @@ namespace osu.Game.Overlays.Wiki.Markdown public partial class WikiMarkdownImageBlock : FillFlowContainer { [Resolved] - private IMarkdownTextFlowComponent parentFlowComponent { get; set; } + private IMarkdownTextFlowComponent parentFlowComponent { get; set; } = null!; private readonly LinkInline linkInline; diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiNoticeContainer.cs b/osu.Game/Overlays/Wiki/Markdown/WikiNoticeContainer.cs index a40bd14878..1ab35b1972 100644 --- a/osu.Game/Overlays/Wiki/Markdown/WikiNoticeContainer.cs +++ b/osu.Game/Overlays/Wiki/Markdown/WikiNoticeContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Markdig.Extensions.Yaml; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -80,7 +78,7 @@ namespace osu.Game.Overlays.Wiki.Markdown private partial class NoticeBox : Container { [Resolved] - private IMarkdownTextFlowComponent parentFlowComponent { get; set; } + private IMarkdownTextFlowComponent parentFlowComponent { get; set; } = null!; public LocalisableString Text { get; set; } diff --git a/osu.Game/Performance/HighPerformanceSession.cs b/osu.Game/Performance/HighPerformanceSession.cs index c113e7a342..07b5e7da98 100644 --- a/osu.Game/Performance/HighPerformanceSession.cs +++ b/osu.Game/Performance/HighPerformanceSession.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Properties/AssemblyInfo.cs b/osu.Game/Properties/AssemblyInfo.cs index dde1af6461..1b77e45891 100644 --- a/osu.Game/Properties/AssemblyInfo.cs +++ b/osu.Game/Properties/AssemblyInfo.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Runtime.CompilerServices; // We publish our internal attributes to other sub-projects of the framework. diff --git a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs index 15b90e5147..e8c4c71913 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using Newtonsoft.Json; diff --git a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs index 38a35ddb3b..f5e826f8c7 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Beatmaps; using osu.Game.Scoring; diff --git a/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs index 76dfca3db7..a654652ef8 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Rulesets.Difficulty { /// diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs index 44abbaaf41..8b8892113b 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs index 6abde64eb7..8fab61ed62 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index 4beba22e05..b43a272324 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index 5f5aba26bb..dc73e35923 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Edit.Checks; diff --git a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs index 20ee409937..324f2068e9 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -25,7 +23,7 @@ namespace osu.Game.Rulesets.Edit private readonly DrawableRuleset drawableRuleset; [Resolved] - private EditorBeatmap beatmap { get; set; } + private EditorBeatmap beatmap { get; set; } = null!; public DrawableEditorRulesetWrapper(DrawableRuleset drawableRuleset) { @@ -43,8 +41,8 @@ namespace osu.Game.Rulesets.Edit Playfield.DisplayJudgements.Value = false; } - [Resolved(canBeNull: true)] - private IEditorChangeHandler changeHandler { get; set; } + [Resolved] + private IEditorChangeHandler? changeHandler { get; set; } protected override void LoadComplete() { diff --git a/osu.Game/Rulesets/Edit/EditorToolboxGroup.cs b/osu.Game/Rulesets/Edit/EditorToolboxGroup.cs index 312ba62b61..f30f5148fe 100644 --- a/osu.Game/Rulesets/Edit/EditorToolboxGroup.cs +++ b/osu.Game/Rulesets/Edit/EditorToolboxGroup.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Overlays; diff --git a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs index 7bf10f6beb..36cbf49885 100644 --- a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs +++ b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; diff --git a/osu.Game/Rulesets/Edit/IBeatmapVerifier.cs b/osu.Game/Rulesets/Edit/IBeatmapVerifier.cs index 826bffef5f..7fc9772598 100644 --- a/osu.Game/Rulesets/Edit/IBeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/IBeatmapVerifier.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Game.Rulesets.Edit.Checks.Components; diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index 6fbd994e23..da44b42831 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs index ad129e068d..002a0aafe6 100644 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osuTK; diff --git a/osu.Game/Rulesets/Edit/SnapType.cs b/osu.Game/Rulesets/Edit/SnapType.cs index f5f9ab0437..cf743f6ace 100644 --- a/osu.Game/Rulesets/Edit/SnapType.cs +++ b/osu.Game/Rulesets/Edit/SnapType.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; namespace osu.Game.Rulesets.Edit diff --git a/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs b/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs index f08b43e72a..2c78561d31 100644 --- a/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs +++ b/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Judgements diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index 770f656e8f..99dce82ec2 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs index 34d1f1f6e9..f001a4cd92 100644 --- a/osu.Game/Rulesets/Judgements/JudgementResult.cs +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using JetBrains.Annotations; using osu.Game.Rulesets.Objects; diff --git a/osu.Game/Rulesets/Objects/HitObjectParser.cs b/osu.Game/Rulesets/Objects/HitObjectParser.cs index 9728a4393b..d3c29d90ce 100644 --- a/osu.Game/Rulesets/Objects/HitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/HitObjectParser.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Rulesets.Objects { public abstract class HitObjectParser diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs index 9facfec96f..12b4812824 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Objects.Types; using osuTK; diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs index 62726019bb..fb1afed3b4 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Objects.Types; using osuTK; diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs index cccb66d92b..014494ec54 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Objects.Legacy.Catch diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs index d95f97624d..54dbd28c76 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs index 6f1968b41d..386eb8d3ee 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK; using osu.Game.Audio; using System.Collections.Generic; diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs index b6594d0206..2fa4766c1d 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Objects.Legacy.Mania diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs index dcbaf22c51..c05aaceb9c 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Objects.Legacy.Mania diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs index 33b390e3ba..069366bad3 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Objects.Types; using osuTK; diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs index 2f8e9dd352..e947690668 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Objects.Types; using osuTK; diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs index d49e9fe9db..e9e5ca8c94 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Objects.Types; using osuTK; diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs index ec8d7971ec..1d5ecb1ef3 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Objects.Legacy.Taiko diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index d32a7cb16d..7013d32cbc 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 13cc6361cf..028f8b6839 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Collections.Specialized; diff --git a/osu.Game/Rulesets/Objects/SyntheticHitObjectEntry.cs b/osu.Game/Rulesets/Objects/SyntheticHitObjectEntry.cs index ee860e82e2..7a9f6948b0 100644 --- a/osu.Game/Rulesets/Objects/SyntheticHitObjectEntry.cs +++ b/osu.Game/Rulesets/Objects/SyntheticHitObjectEntry.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Objects diff --git a/osu.Game/Rulesets/Objects/Types/IHasDisplayColour.cs b/osu.Game/Rulesets/Objects/Types/IHasDisplayColour.cs index 89ee5022bf..691418ec48 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasDisplayColour.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasDisplayColour.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osuTK.Graphics; diff --git a/osu.Game/Rulesets/Objects/Types/IHasPath.cs b/osu.Game/Rulesets/Objects/Types/IHasPath.cs index 46834a55dd..5a3f270f54 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasPath.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasPath.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Rulesets.Objects.Types { public interface IHasPath : IHasDistance diff --git a/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs b/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs index 536707e95f..279946b44e 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK; namespace osu.Game.Rulesets.Objects.Types diff --git a/osu.Game/Rulesets/Objects/Types/IHasPosition.cs b/osu.Game/Rulesets/Objects/Types/IHasPosition.cs index 281f619ba5..8948fe59a9 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasPosition.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasPosition.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK; namespace osu.Game.Rulesets.Objects.Types diff --git a/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs b/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs index 821a6de520..2a4215b960 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Audio; using System.Collections.Generic; diff --git a/osu.Game/Rulesets/RulesetLoadException.cs b/osu.Game/Rulesets/RulesetLoadException.cs index 6fee8f446b..803c756b41 100644 --- a/osu.Game/Rulesets/RulesetLoadException.cs +++ b/osu.Game/Rulesets/RulesetLoadException.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; namespace osu.Game.Rulesets diff --git a/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs index af6e825b06..422bf8ea79 100644 --- a/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Rulesets.Scoring { /// diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 2fde73d5a2..b4bdd8a1ea 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 99129fcf96..2d008b58ba 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Diagnostics; diff --git a/osu.Game/Rulesets/UI/GameplayCursorContainer.cs b/osu.Game/Rulesets/UI/GameplayCursorContainer.cs index cbce397d1e..0ce3f76384 100644 --- a/osu.Game/Rulesets/UI/GameplayCursorContainer.cs +++ b/osu.Game/Rulesets/UI/GameplayCursorContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; diff --git a/osu.Game/Rulesets/UI/IHitObjectContainer.cs b/osu.Game/Rulesets/UI/IHitObjectContainer.cs index 74fd7dee81..6dcb0944be 100644 --- a/osu.Game/Rulesets/UI/IHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/IHitObjectContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Game.Rulesets.Objects.Drawables; diff --git a/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs b/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs index b842e708b0..f5739ee525 100644 --- a/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs +++ b/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using JetBrains.Annotations; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; diff --git a/osu.Game/Rulesets/UI/JudgementContainer.cs b/osu.Game/Rulesets/UI/JudgementContainer.cs index 7181e80206..886dd34fc7 100644 --- a/osu.Game/Rulesets/UI/JudgementContainer.cs +++ b/osu.Game/Rulesets/UI/JudgementContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Judgements; diff --git a/osu.Game/Rulesets/UI/PlayfieldAdjustmentContainer.cs b/osu.Game/Rulesets/UI/PlayfieldAdjustmentContainer.cs index 0f440adef8..9339602ac6 100644 --- a/osu.Game/Rulesets/UI/PlayfieldAdjustmentContainer.cs +++ b/osu.Game/Rulesets/UI/PlayfieldAdjustmentContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Rulesets/UI/PlayfieldBorder.cs b/osu.Game/Rulesets/UI/PlayfieldBorder.cs index 18bd5b9b93..e87421fc88 100644 --- a/osu.Game/Rulesets/UI/PlayfieldBorder.cs +++ b/osu.Game/Rulesets/UI/PlayfieldBorder.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Rulesets/UI/PlayfieldBorderStyle.cs b/osu.Game/Rulesets/UI/PlayfieldBorderStyle.cs index 79f3a2ca84..503bc8fd99 100644 --- a/osu.Game/Rulesets/UI/PlayfieldBorderStyle.cs +++ b/osu.Game/Rulesets/UI/PlayfieldBorderStyle.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs index c957a84eb1..b0bde50cae 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Rulesets.UI.Scrolling.Algorithms { public class ConstantScrollAlgorithm : IScrollAlgorithm diff --git a/osu.Game/Rulesets/UI/Scrolling/IScrollingInfo.cs b/osu.Game/Rulesets/UI/Scrolling/IScrollingInfo.cs index e00f0ffe5d..cd85932599 100644 --- a/osu.Game/Rulesets/UI/Scrolling/IScrollingInfo.cs +++ b/osu.Game/Rulesets/UI/Scrolling/IScrollingInfo.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI.Scrolling.Algorithms; diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 7d141113df..1a17349d12 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Rulesets.Objects.Drawables; @@ -20,7 +18,7 @@ namespace osu.Game.Rulesets.UI.Scrolling public new ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)base.HitObjectContainer; [Resolved] - public IScrollingInfo ScrollingInfo { get; private set; } + public IScrollingInfo ScrollingInfo { get; private set; } = null!; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Scoring/HitResultDisplayStatistic.cs b/osu.Game/Scoring/HitResultDisplayStatistic.cs index 20deff4875..59e074fb5f 100644 --- a/osu.Game/Scoring/HitResultDisplayStatistic.cs +++ b/osu.Game/Scoring/HitResultDisplayStatistic.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game/Scoring/IScoreInfo.cs b/osu.Game/Scoring/IScoreInfo.cs index ffc30384d2..3644d099d9 100644 --- a/osu.Game/Scoring/IScoreInfo.cs +++ b/osu.Game/Scoring/IScoreInfo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Game.Beatmaps; using osu.Game.Database; diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs index 52dec20b32..e298d51ccb 100644 --- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game/Scoring/Score.cs b/osu.Game/Scoring/Score.cs index 06bc3edd37..3323706ac1 100644 --- a/osu.Game/Scoring/Score.cs +++ b/osu.Game/Scoring/Score.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Replays; using osu.Game.Utils; diff --git a/osu.Game/Scoring/ScoreRank.cs b/osu.Game/Scoring/ScoreRank.cs index a1916953c4..327e4191d7 100644 --- a/osu.Game/Scoring/ScoreRank.cs +++ b/osu.Game/Scoring/ScoreRank.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Screens/BackgroundScreenStack.cs b/osu.Game/Screens/BackgroundScreenStack.cs index ca0dad83c8..6ebc97ebbb 100644 --- a/osu.Game/Screens/BackgroundScreenStack.cs +++ b/osu.Game/Screens/BackgroundScreenStack.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Screens; diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenBlack.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenBlack.cs index 09778c5cdf..742d149580 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenBlack.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenBlack.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenCustom.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenCustom.cs index 3c8ed6fe76..14331c1978 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenCustom.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenCustom.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Graphics.Backgrounds; namespace osu.Game.Screens.Backgrounds diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorPresetCollection.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorPresetCollection.cs index 67b346fb64..56df0552cc 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorPresetCollection.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorPresetCollection.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index 602ed6f627..e33ef66007 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -19,7 +17,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public abstract partial class CircularDistanceSnapGrid : DistanceSnapGrid { [Resolved] - private EditorClock editorClock { get; set; } + private EditorClock editorClock { get; set; } = null!; protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) : base(referenceObject, startPosition, startTime, endTime) @@ -127,10 +125,10 @@ namespace osu.Game.Screens.Edit.Compose.Components private partial class Ring : CircularProgress { [Resolved] - private IDistanceSnapProvider snapProvider { get; set; } + private IDistanceSnapProvider snapProvider { get; set; } = null!; - [Resolved(canBeNull: true)] - private EditorClock editorClock { get; set; } + [Resolved] + private EditorClock? editorClock { get; set; } private readonly HitObject referenceObject; diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index d618541685..a73278a61e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -28,7 +26,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public const string HIT_BANK_AUTO = "auto"; [Resolved] - protected EditorBeatmap EditorBeatmap { get; private set; } + protected EditorBeatmap EditorBeatmap { get; private set; } = null!; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs index 849a526556..0edaaf9825 100644 --- a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,7 +16,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public sealed partial class HitObjectOrderedSelectionContainer : Container> { [Resolved] - private EditorBeatmap editorBeatmap { get; set; } + private EditorBeatmap editorBeatmap { get; set; } = null!; protected override void LoadComplete() { diff --git a/osu.Game/Screens/Edit/Compose/Components/MoveSelectionEvent.cs b/osu.Game/Screens/Edit/Compose/Components/MoveSelectionEvent.cs index 46d948f8b6..4515e4d7be 100644 --- a/osu.Game/Screens/Edit/Compose/Components/MoveSelectionEvent.cs +++ b/osu.Game/Screens/Edit/Compose/Components/MoveSelectionEvent.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Edit; using osuTK; diff --git a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs index 06b73c8af4..063ea23281 100644 --- a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs index 44daf70577..aee3cffbfd 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineButton.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineButton.cs index c94de0fe67..767854252e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineButton.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs index 29983c9cbf..cd97b293ba 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Specialized; using System.Diagnostics; using System.Linq; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs index 257cc9e635..fc3ef92bf5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs index a1dfd0718b..c16a948822 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -20,7 +18,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private double? startTime; [Resolved] - private Timeline timeline { get; set; } + private Timeline timeline { get; set; } = null!; protected override Drawable CreateBox() => new Box { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 6a0688e19c..7e7bef8cf2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Diagnostics; using osu.Framework.Allocation; @@ -21,19 +19,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public partial class TimelineTickDisplay : TimelinePart { [Resolved] - private EditorBeatmap beatmap { get; set; } + private EditorBeatmap beatmap { get; set; } = null!; [Resolved] - private Bindable working { get; set; } + private Bindable working { get; set; } = null!; [Resolved] - private BindableBeatDivisor beatDivisor { get; set; } - - [Resolved(CanBeNull = true)] - private IEditorChangeHandler changeHandler { get; set; } + private BindableBeatDivisor beatDivisor { get; set; } = null!; [Resolved] - private OsuColour colours { get; set; } + private IEditorChangeHandler? changeHandler { get; set; } + + [Resolved] + private OsuColour colours { get; set; } = null!; public TimelineTickDisplay() { @@ -72,8 +70,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// private float? nextMaxTick; - [Resolved(canBeNull: true)] - private Timeline timeline { get; set; } + [Resolved] + private Timeline? timeline { get; set; } protected override void Update() { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs index 4191864e5c..2a4ad66918 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps.ControlPoints; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index 951f4129d4..848c8f9a0f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -41,8 +39,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private bool isZoomSetUp; - [Resolved(canBeNull: true)] - private IFrameBasedClock editorClock { get; set; } + [Resolved] + private IFrameBasedClock? editorClock { get; set; } private readonly LayoutValue zoomedContentWidthCache = new LayoutValue(Invalidation.DrawSize); diff --git a/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs b/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs index 46d9555e0c..57960a76a1 100644 --- a/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs +++ b/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Rulesets.Objects; diff --git a/osu.Game/Screens/Edit/EditorClipboard.cs b/osu.Game/Screens/Edit/EditorClipboard.cs index f749f4bad6..af303618fb 100644 --- a/osu.Game/Screens/Edit/EditorClipboard.cs +++ b/osu.Game/Screens/Edit/EditorClipboard.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; namespace osu.Game.Screens.Edit diff --git a/osu.Game/Screens/Edit/EditorRoundedScreenSettings.cs b/osu.Game/Screens/Edit/EditorRoundedScreenSettings.cs index 1c083b4fab..510c27e8c6 100644 --- a/osu.Game/Screens/Edit/EditorRoundedScreenSettings.cs +++ b/osu.Game/Screens/Edit/EditorRoundedScreenSettings.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Screens/Edit/EditorScreen.cs b/osu.Game/Screens/Edit/EditorScreen.cs index 069a5490bb..b39c0cf5f3 100644 --- a/osu.Game/Screens/Edit/EditorScreen.cs +++ b/osu.Game/Screens/Edit/EditorScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -17,7 +15,7 @@ namespace osu.Game.Screens.Edit public abstract partial class EditorScreen : VisibilityContainer { [Resolved] - protected EditorBeatmap EditorBeatmap { get; private set; } + protected EditorBeatmap EditorBeatmap { get; private set; } = null!; protected override Container Content => content; private readonly Container content; diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs index a74d97cdc7..bb151e4a45 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; @@ -14,7 +12,7 @@ namespace osu.Game.Screens.Edit.GameplayTest public partial class EditorPlayerLoader : PlayerLoader { [Resolved] - private OsuLogo osuLogo { get; set; } + private OsuLogo osuLogo { get; set; } = null!; public EditorPlayerLoader(Editor editor) : base(() => new EditorPlayer(editor)) diff --git a/osu.Game/Screens/Edit/GameplayTest/SaveBeforeGameplayTestDialog.cs b/osu.Game/Screens/Edit/GameplayTest/SaveBeforeGameplayTestDialog.cs index 5a5572b508..eb1df43ddd 100644 --- a/osu.Game/Screens/Edit/GameplayTest/SaveBeforeGameplayTestDialog.cs +++ b/osu.Game/Screens/Edit/GameplayTest/SaveBeforeGameplayTestDialog.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics.Sprites; using osu.Game.Overlays.Dialog; diff --git a/osu.Game/Screens/Edit/HitAnimationsMenuItem.cs b/osu.Game/Screens/Edit/HitAnimationsMenuItem.cs index 3e1e0c4cfe..ee64a53301 100644 --- a/osu.Game/Screens/Edit/HitAnimationsMenuItem.cs +++ b/osu.Game/Screens/Edit/HitAnimationsMenuItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Screens/Edit/Verify/InterpretationSection.cs b/osu.Game/Screens/Edit/Verify/InterpretationSection.cs index 5b6eea098c..b16e3750bf 100644 --- a/osu.Game/Screens/Edit/Verify/InterpretationSection.cs +++ b/osu.Game/Screens/Edit/Verify/InterpretationSection.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; diff --git a/osu.Game/Screens/Edit/Verify/IssueSettings.cs b/osu.Game/Screens/Edit/Verify/IssueSettings.cs index e8275c3684..6d3c0520a2 100644 --- a/osu.Game/Screens/Edit/Verify/IssueSettings.cs +++ b/osu.Game/Screens/Edit/Verify/IssueSettings.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Graphics; diff --git a/osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs b/osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs index 5b1d7142e4..9dc0ea0d07 100644 --- a/osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs +++ b/osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Screens/IHandlePresentBeatmap.cs b/osu.Game/Screens/IHandlePresentBeatmap.cs index 62cd2c3d3e..323e3b1c0c 100644 --- a/osu.Game/Screens/IHandlePresentBeatmap.cs +++ b/osu.Game/Screens/IHandlePresentBeatmap.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Beatmaps; using osu.Game.Rulesets; diff --git a/osu.Game/Screens/IHasSubScreenStack.cs b/osu.Game/Screens/IHasSubScreenStack.cs index 325702313b..0fcf21ef2b 100644 --- a/osu.Game/Screens/IHasSubScreenStack.cs +++ b/osu.Game/Screens/IHasSubScreenStack.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Screens; namespace osu.Game.Screens diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index a5739a41b1..cceede5424 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Screens; using osu.Game.Beatmaps; diff --git a/osu.Game/Screens/Menu/ExitConfirmOverlay.cs b/osu.Game/Screens/Menu/ExitConfirmOverlay.cs index bc2f6ea00f..64b9bd52e8 100644 --- a/osu.Game/Screens/Menu/ExitConfirmOverlay.cs +++ b/osu.Game/Screens/Menu/ExitConfirmOverlay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; diff --git a/osu.Game/Screens/Menu/SongTicker.cs b/osu.Game/Screens/Menu/SongTicker.cs index bac7e15461..3bdc0efe19 100644 --- a/osu.Game/Screens/Menu/SongTicker.cs +++ b/osu.Game/Screens/Menu/SongTicker.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -20,7 +18,7 @@ namespace osu.Game.Screens.Menu private const int fade_duration = 800; [Resolved] - private Bindable beatmap { get; set; } + private Bindable beatmap { get; set; } = null!; private readonly OsuSpriteText title, artist; diff --git a/osu.Game/Screens/Menu/StorageErrorDialog.cs b/osu.Game/Screens/Menu/StorageErrorDialog.cs index ba05ad8b76..dd43289873 100644 --- a/osu.Game/Screens/Menu/StorageErrorDialog.cs +++ b/osu.Game/Screens/Menu/StorageErrorDialog.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; @@ -15,7 +13,7 @@ namespace osu.Game.Screens.Menu public partial class StorageErrorDialog : PopupDialog { [Resolved] - private IDialogOverlay dialogOverlay { get; set; } + private IDialogOverlay dialogOverlay { get; set; } = null!; public StorageErrorDialog(OsuStorage storage, OsuStorageError error) { diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapDetailAreaPlaylistTabItem.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapDetailAreaPlaylistTabItem.cs index 7c48fc0871..41b994ea32 100644 --- a/osu.Game/Screens/OnlinePlay/Components/BeatmapDetailAreaPlaylistTabItem.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapDetailAreaPlaylistTabItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay.Components diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs index ebcc08360e..7c57f5b4f5 100644 --- a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -49,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Components } [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; private void updateText() { diff --git a/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs b/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs index 3f7f38f3bc..97716759c3 100644 --- a/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs +++ b/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; diff --git a/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs index 77e461ce41..d24ad74a68 100644 --- a/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs +++ b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; @@ -41,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Components } [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs index 0e2ce6703f..09a3602cdd 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs b/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs index f8dcd7b75d..fc86cbbbdd 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components diff --git a/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs index 4fdf41d0f7..e6999771d3 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs index 772c8c4278..813e243449 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Cursor; diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs index 395a77b9e6..0ba7f20f1c 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Online; using osu.Game.Online.API; @@ -12,9 +10,9 @@ namespace osu.Game.Screens.OnlinePlay.Components public abstract partial class RoomPollingComponent : PollingComponent { [Resolved] - protected IAPIProvider API { get; private set; } + protected IAPIProvider API { get; private set; } = null!; [Resolved] - protected IRoomManager RoomManager { get; private set; } + protected IRoomManager RoomManager { get; private set; } = null!; } } diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 98f3df525d..920aff13a8 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Screens/OnlinePlay/IOnlinePlaySubScreen.cs b/osu.Game/Screens/OnlinePlay/IOnlinePlaySubScreen.cs index f32ead5a11..c528e3952e 100644 --- a/osu.Game/Screens/OnlinePlay/IOnlinePlaySubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/IOnlinePlaySubScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Screens.OnlinePlay { public interface IOnlinePlaySubScreen : IOsuScreen diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs index c25dd6f158..844991095e 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/MatchTypePill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/MatchTypePill.cs index 35e0482f2b..e30d673b26 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/MatchTypePill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/MatchTypePill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Game.Online.Rooms; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/PillContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/PillContainer.cs index 263261143d..b473ea82c6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/PillContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/PillContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs index d1365c02f3..fe5ccb4f09 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using Humanizer; using osu.Framework.Extensions.LocalisationExtensions; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/QueueModePill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/QueueModePill.cs index 208c11c155..23f4ecf8db 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/QueueModePill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/QueueModePill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Game.Online.Multiplayer; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomSpecialCategoryPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomSpecialCategoryPill.cs index 10f6e59260..9b8954bb33 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomSpecialCategoryPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomSpecialCategoryPill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics.Sprites; @@ -14,7 +12,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public partial class RoomSpecialCategoryPill : OnlinePlayPill { [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; protected override FontUsage Font => base.Font.With(weight: FontWeight.SemiBold); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs index 463b883f11..53fbf670e1 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; namespace osu.Game.Screens.OnlinePlay.Lounge.Components diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs index ca9917ad00..aae82b6721 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -19,7 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public partial class RoomStatusPill : OnlinePlayPill { [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; protected override FontUsage Font => base.Font.With(weight: FontWeight.SemiBold); diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/CreateRoomButton.cs b/osu.Game/Screens/OnlinePlay/Match/Components/CreateRoomButton.cs index 0251dba6ce..3788f4c0b2 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/CreateRoomButton.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/CreateRoomButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Input; using osu.Framework.Input.Bindings; diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs index 55d39407b0..8dc1704fcd 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.Chat; @@ -14,8 +12,8 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components { private readonly IBindable channelId = new Bindable(); - [Resolved(CanBeNull = true)] - private ChannelManager channelManager { get; set; } + [Resolved] + private ChannelManager? channelManager { get; set; } private readonly Room room; private readonly bool leaveChannelOnDispose; diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomBackgroundScreen.cs index c9e51d376c..982275f96a 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomBackgroundScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomBackgroundScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayMatchScoreDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayMatchScoreDisplay.cs index 8c08390c73..b4373d728f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayMatchScoreDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayMatchScoreDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Screens.Play.HUD; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs index a19f61787b..d18bb011f0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs index a5589c48b9..e5d94c5358 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 164d1c9a4b..66ae814444 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Logging; @@ -16,7 +14,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public partial class Multiplayer : OnlinePlayScreen { [Resolved] - private MultiplayerClient client { get; set; } + private MultiplayerClient client { get; set; } = null!; protected override void LoadComplete() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs index de19d3a0e9..9708a94cd7 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Online.Rooms; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Playlists; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsListHeader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsListHeader.cs index 7f4e3360e4..79c6fb33cd 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsListHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsListHeader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Online.Multiplayer; using osu.Game.Resources.Localisation.Web; @@ -13,7 +11,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants public partial class ParticipantsListHeader : OverlinedHeader { [Resolved] - private MultiplayerClient client { get; set; } + private MultiplayerClient client { get; set; } = null!; public ParticipantsListHeader() : base(RankingsStrings.SpotlightParticipants) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs index eb55b0d18a..9a43e96a50 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using JetBrains.Annotations; using osu.Framework.Allocation; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs index 82d4cf5caf..771a8c0de4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs index 2f4ed35392..934c22c918 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs index 7ecb7d954e..2e3e2e1dc0 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Screens; namespace osu.Game.Screens.OnlinePlay diff --git a/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs index 9507169e0f..d56ef9ef0c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Screens.OnlinePlay.Match.Components; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs index f9324840dc..f1d2384c2f 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Screens.OnlinePlay.Lounge; namespace osu.Game.Screens.OnlinePlay.Playlists diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsPlaylist.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsPlaylist.cs index 736f09584b..2ca1f4cd1f 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsPlaylist.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; diff --git a/osu.Game/Screens/OsuScreenStack.cs b/osu.Game/Screens/OsuScreenStack.cs index dffbbdbc55..ef579fac85 100644 --- a/osu.Game/Screens/OsuScreenStack.cs +++ b/osu.Game/Screens/OsuScreenStack.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; diff --git a/osu.Game/Screens/Play/Break/BlurredIcon.cs b/osu.Game/Screens/Play/Break/BlurredIcon.cs index 6ce1c2e686..2bf59ea63b 100644 --- a/osu.Game/Screens/Play/Break/BlurredIcon.cs +++ b/osu.Game/Screens/Play/Break/BlurredIcon.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Screens/Play/Break/BreakArrows.cs b/osu.Game/Screens/Play/Break/BreakArrows.cs index f0f1e8cc3d..41277c7557 100644 --- a/osu.Game/Screens/Play/Break/BreakArrows.cs +++ b/osu.Game/Screens/Play/Break/BreakArrows.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Screens/Play/Break/BreakInfo.cs b/osu.Game/Screens/Play/Break/BreakInfo.cs index f99c1d1817..ef453405b5 100644 --- a/osu.Game/Screens/Play/Break/BreakInfo.cs +++ b/osu.Game/Screens/Play/Break/BreakInfo.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; diff --git a/osu.Game/Screens/Play/Break/GlowIcon.cs b/osu.Game/Screens/Play/Break/GlowIcon.cs index 595c4dd494..8e2b9da0ad 100644 --- a/osu.Game/Screens/Play/Break/GlowIcon.cs +++ b/osu.Game/Screens/Play/Break/GlowIcon.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Screens/Play/Break/RemainingTimeCounter.cs b/osu.Game/Screens/Play/Break/RemainingTimeCounter.cs index da83f8c29f..3ac0a493da 100644 --- a/osu.Game/Screens/Play/Break/RemainingTimeCounter.cs +++ b/osu.Game/Screens/Play/Break/RemainingTimeCounter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs index eb3c71afbb..4a0e8b4f39 100644 --- a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Skinning; diff --git a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs index 2c43905a46..c4d04c5580 100644 --- a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; diff --git a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs index 7cc2dc1751..2a17559503 100644 --- a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index 7a73eb1657..19ede5533f 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -22,7 +20,7 @@ namespace osu.Game.Screens.Play.HUD private readonly Bindable showHealthBar = new Bindable(true); [Resolved] - protected HealthProcessor HealthProcessor { get; private set; } + protected HealthProcessor HealthProcessor { get; private set; } = null!; public Bindable Current { get; } = new BindableDouble(1) { @@ -34,8 +32,8 @@ namespace osu.Game.Screens.Play.HUD { } - [Resolved(canBeNull: true)] - private HUDOverlay hudOverlay { get; set; } + [Resolved] + private HUDOverlay? hudOverlay { get; set; } protected override void LoadComplete() { diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 8b2b8f9464..579a0d163a 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using osu.Framework.Bindables; diff --git a/osu.Game/Screens/Play/HUD/ModFlowDisplay.cs b/osu.Game/Screens/Play/HUD/ModFlowDisplay.cs index 38027c64ac..0b2ce10ac3 100644 --- a/osu.Game/Screens/Play/HUD/ModFlowDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModFlowDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using osu.Framework.Bindables; diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index 064d2071ce..b6b385e262 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; diff --git a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs index 4ceca817e2..e3034b2442 100644 --- a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs +++ b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -30,7 +28,7 @@ namespace osu.Game.Screens.Play.HUD private readonly Bindable valid = new Bindable(); [Resolved] - private ScoreProcessor scoreProcessor { get; set; } + private ScoreProcessor scoreProcessor { get; set; } = null!; public UnstableRateCounter() { diff --git a/osu.Game/Screens/Play/HotkeyExitOverlay.cs b/osu.Game/Screens/Play/HotkeyExitOverlay.cs index 4c1265c699..bcd9bd7cd6 100644 --- a/osu.Game/Screens/Play/HotkeyExitOverlay.cs +++ b/osu.Game/Screens/Play/HotkeyExitOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; diff --git a/osu.Game/Screens/Play/HotkeyRetryOverlay.cs b/osu.Game/Screens/Play/HotkeyRetryOverlay.cs index 582b5a1691..11d0b4f84f 100644 --- a/osu.Game/Screens/Play/HotkeyRetryOverlay.cs +++ b/osu.Game/Screens/Play/HotkeyRetryOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; diff --git a/osu.Game/Screens/Play/ILocalUserPlayInfo.cs b/osu.Game/Screens/Play/ILocalUserPlayInfo.cs index e4328b2c78..2d181a09d4 100644 --- a/osu.Game/Screens/Play/ILocalUserPlayInfo.cs +++ b/osu.Game/Screens/Play/ILocalUserPlayInfo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Screens/Play/PlayerConfiguration.cs b/osu.Game/Screens/Play/PlayerConfiguration.cs index b82925ccb8..122e25f406 100644 --- a/osu.Game/Screens/Play/PlayerConfiguration.cs +++ b/osu.Game/Screens/Play/PlayerConfiguration.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Screens.Play { public class PlayerConfiguration diff --git a/osu.Game/Screens/Play/PlayerSettings/DiscussionSettings.cs b/osu.Game/Screens/Play/PlayerSettings/DiscussionSettings.cs index 7c76936621..f64861cfa5 100644 --- a/osu.Game/Screens/Play/PlayerSettings/DiscussionSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/DiscussionSettings.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; diff --git a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs index 13e5b66a70..cf261ba49b 100644 --- a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index cb6fcb2413..4753effdb0 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs index 45009684a6..88b778fafb 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Screens/Play/ReplayPlayerLoader.cs b/osu.Game/Screens/Play/ReplayPlayerLoader.cs index 1c9d694325..7da06fe506 100644 --- a/osu.Game/Screens/Play/ReplayPlayerLoader.cs +++ b/osu.Game/Screens/Play/ReplayPlayerLoader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Screens; using osu.Game.Scoring; diff --git a/osu.Game/Screens/Play/SpectatorPlayerLoader.cs b/osu.Game/Screens/Play/SpectatorPlayerLoader.cs index 3830443ce8..8f2bcfe046 100644 --- a/osu.Game/Screens/Play/SpectatorPlayerLoader.cs +++ b/osu.Game/Screens/Play/SpectatorPlayerLoader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Screens; using osu.Game.Scoring; diff --git a/osu.Game/Screens/Play/SpectatorResultsScreen.cs b/osu.Game/Screens/Play/SpectatorResultsScreen.cs index b54dbb387a..67ec1373df 100644 --- a/osu.Game/Screens/Play/SpectatorResultsScreen.cs +++ b/osu.Game/Screens/Play/SpectatorResultsScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Game.Online.Spectator; @@ -19,7 +17,7 @@ namespace osu.Game.Screens.Play } [Resolved] - private SpectatorClient spectatorClient { get; set; } + private SpectatorClient spectatorClient { get; set; } = null!; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Screens/Ranking/AspectContainer.cs b/osu.Game/Screens/Ranking/AspectContainer.cs index 9ec2a15044..a26bb8fe43 100644 --- a/osu.Game/Screens/Ranking/AspectContainer.cs +++ b/osu.Game/Screens/Ranking/AspectContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index 402322c611..195cd03e9b 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -36,7 +34,7 @@ namespace osu.Game.Screens.Ranking.Contracted private readonly ScoreInfo score; [Resolved] - private ScoreManager scoreManager { get; set; } + private ScoreManager scoreManager { get; set; } = null!; /// /// Creates a new . diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs index 32f2eb2fa5..244acbe8b1 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs index 863c450617..384e5661b4 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticCounter.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticCounter.cs index ecadc9eed6..b279c8107c 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticCounter.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticCounter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; diff --git a/osu.Game/Screens/Ranking/RetryButton.cs b/osu.Game/Screens/Ranking/RetryButton.cs index c7d2416e29..d977f25323 100644 --- a/osu.Game/Screens/Ranking/RetryButton.cs +++ b/osu.Game/Screens/Ranking/RetryButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; @@ -18,8 +16,8 @@ namespace osu.Game.Screens.Ranking { private readonly Box background; - [Resolved(canBeNull: true)] - private Player player { get; set; } + [Resolved] + private Player? player { get; set; } public RetryButton() { diff --git a/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs b/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs index ec153cbd63..f5a26ef754 100644 --- a/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs +++ b/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs index bb9905d29c..fb7107cc88 100644 --- a/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs +++ b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index d3327224dc..4202b2158e 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Diagnostics.CodeAnalysis; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index c5bdc6f6f5..6a595bf05c 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using JetBrains.Annotations; using osu.Framework.Graphics; diff --git a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs index de01668029..cc3535a426 100644 --- a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs +++ b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game/Screens/Select/BeatmapDetailAreaDetailTabItem.cs b/osu.Game/Screens/Select/BeatmapDetailAreaDetailTabItem.cs index d6b076f30b..4ff2600a72 100644 --- a/osu.Game/Screens/Select/BeatmapDetailAreaDetailTabItem.cs +++ b/osu.Game/Screens/Select/BeatmapDetailAreaDetailTabItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Screens.Select { public class BeatmapDetailAreaDetailTabItem : BeatmapDetailAreaTabItem diff --git a/osu.Game/Screens/Select/BeatmapDetailAreaLeaderboardTabItem.cs b/osu.Game/Screens/Select/BeatmapDetailAreaLeaderboardTabItem.cs index 6efadc77b3..8dbe5b8bea 100644 --- a/osu.Game/Screens/Select/BeatmapDetailAreaLeaderboardTabItem.cs +++ b/osu.Game/Screens/Select/BeatmapDetailAreaLeaderboardTabItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; namespace osu.Game.Screens.Select diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeBackground.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeBackground.cs index d5d258704b..50ec446c4f 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeBackground.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeBackground.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; diff --git a/osu.Game/Screens/Select/Filter/GroupMode.cs b/osu.Game/Screens/Select/Filter/GroupMode.cs index 8e2b9271b0..d794c215a3 100644 --- a/osu.Game/Screens/Select/Filter/GroupMode.cs +++ b/osu.Game/Screens/Select/Filter/GroupMode.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; namespace osu.Game.Screens.Select.Filter diff --git a/osu.Game/Screens/Select/Filter/SortMode.cs b/osu.Game/Screens/Select/Filter/SortMode.cs index c77bdbfbc6..7f2b33adbe 100644 --- a/osu.Game/Screens/Select/Filter/SortMode.cs +++ b/osu.Game/Screens/Select/Filter/SortMode.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Screens/Select/FooterButtonOptions.cs b/osu.Game/Screens/Select/FooterButtonOptions.cs index e56efcb458..532051369b 100644 --- a/osu.Game/Screens/Select/FooterButtonOptions.cs +++ b/osu.Game/Screens/Select/FooterButtonOptions.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Game.Graphics; diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs index b8840b124a..5bcb4c27a7 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs index 0d3e1238f3..045a518525 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Screens/Spectate/SpectatorGameplayState.cs b/osu.Game/Screens/Spectate/SpectatorGameplayState.cs index 498363adef..1ee328a307 100644 --- a/osu.Game/Screens/Spectate/SpectatorGameplayState.cs +++ b/osu.Game/Screens/Spectate/SpectatorGameplayState.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Scoring; diff --git a/osu.Game/Screens/StartupScreen.cs b/osu.Game/Screens/StartupScreen.cs index 84ef3eac78..9e04a238eb 100644 --- a/osu.Game/Screens/StartupScreen.cs +++ b/osu.Game/Screens/StartupScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Overlays; namespace osu.Game.Screens diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index f460a3d31a..f1c99a315d 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; diff --git a/osu.Game/Storyboards/CommandLoop.cs b/osu.Game/Storyboards/CommandLoop.cs index 29e034d86c..480d69c12f 100644 --- a/osu.Game/Storyboards/CommandLoop.cs +++ b/osu.Game/Storyboards/CommandLoop.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs index d198ed68bd..0b96db6861 100644 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ b/osu.Game/Storyboards/CommandTimelineGroup.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osuTK; using osuTK.Graphics; diff --git a/osu.Game/Storyboards/CommandTrigger.cs b/osu.Game/Storyboards/CommandTrigger.cs index 50f3f0ef49..011f345df2 100644 --- a/osu.Game/Storyboards/CommandTrigger.cs +++ b/osu.Game/Storyboards/CommandTrigger.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Storyboards { public class CommandTrigger : CommandTimelineGroup diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs index 6fc8d124c7..38e7ff1c70 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Threading; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index c281d23804..830b6a5caa 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -30,8 +28,8 @@ namespace osu.Game.Storyboards.Drawables LifetimeStart = sampleInfo.StartTime; } - [Resolved(CanBeNull = true)] - private IReadOnlyList mods { get; set; } + [Resolved] + private IReadOnlyList? mods { get; set; } protected override void SkinChanged(ISkinSource skin) { diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 400d33481c..63f644886a 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -86,7 +84,7 @@ namespace osu.Game.Storyboards.Drawables } [Resolved] - private ISkinSource skin { get; set; } + private ISkinSource skin { get; set; } = null!; [BackgroundDependencyLoader] private void load(TextureStore textureStore, Storyboard storyboard) diff --git a/osu.Game/Storyboards/Drawables/DrawablesExtensions.cs b/osu.Game/Storyboards/Drawables/DrawablesExtensions.cs index 779c8384c5..bbc55a336d 100644 --- a/osu.Game/Storyboards/Drawables/DrawablesExtensions.cs +++ b/osu.Game/Storyboards/Drawables/DrawablesExtensions.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; diff --git a/osu.Game/Storyboards/Drawables/IFlippable.cs b/osu.Game/Storyboards/Drawables/IFlippable.cs index aceb5c041c..165b3d97cc 100644 --- a/osu.Game/Storyboards/Drawables/IFlippable.cs +++ b/osu.Game/Storyboards/Drawables/IFlippable.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; diff --git a/osu.Game/Storyboards/Drawables/IVectorScalable.cs b/osu.Game/Storyboards/Drawables/IVectorScalable.cs index 3b43a35a90..60a297e126 100644 --- a/osu.Game/Storyboards/Drawables/IVectorScalable.cs +++ b/osu.Game/Storyboards/Drawables/IVectorScalable.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; using osuTK; diff --git a/osu.Game/Storyboards/IStoryboardElement.cs b/osu.Game/Storyboards/IStoryboardElement.cs index 7e83f8b692..9a059991e6 100644 --- a/osu.Game/Storyboards/IStoryboardElement.cs +++ b/osu.Game/Storyboards/IStoryboardElement.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; namespace osu.Game.Storyboards diff --git a/osu.Game/Storyboards/IStoryboardElementWithDuration.cs b/osu.Game/Storyboards/IStoryboardElementWithDuration.cs index 9eed139ad4..3e0f7fb576 100644 --- a/osu.Game/Storyboards/IStoryboardElementWithDuration.cs +++ b/osu.Game/Storyboards/IStoryboardElementWithDuration.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Storyboards { /// diff --git a/osu.Game/Storyboards/StoryboardExtensions.cs b/osu.Game/Storyboards/StoryboardExtensions.cs index e5cafc152b..04c7196315 100644 --- a/osu.Game/Storyboards/StoryboardExtensions.cs +++ b/osu.Game/Storyboards/StoryboardExtensions.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osuTK; diff --git a/osu.Game/Storyboards/StoryboardLayer.cs b/osu.Game/Storyboards/StoryboardLayer.cs index 2ab8d9fc2a..fa9d4ebfea 100644 --- a/osu.Game/Storyboards/StoryboardLayer.cs +++ b/osu.Game/Storyboards/StoryboardLayer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Storyboards.Drawables; using System.Collections.Generic; diff --git a/osu.Game/Storyboards/StoryboardSample.cs b/osu.Game/Storyboards/StoryboardSample.cs index 752d086993..5d6ce215f5 100644 --- a/osu.Game/Storyboards/StoryboardSample.cs +++ b/osu.Game/Storyboards/StoryboardSample.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Audio; diff --git a/osu.Game/Storyboards/StoryboardVideo.cs b/osu.Game/Storyboards/StoryboardVideo.cs index 04ff941397..4652e45852 100644 --- a/osu.Game/Storyboards/StoryboardVideo.cs +++ b/osu.Game/Storyboards/StoryboardVideo.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Storyboards.Drawables; diff --git a/osu.Game/Storyboards/StoryboardVideoLayer.cs b/osu.Game/Storyboards/StoryboardVideoLayer.cs index f08c02cfd2..f780604029 100644 --- a/osu.Game/Storyboards/StoryboardVideoLayer.cs +++ b/osu.Game/Storyboards/StoryboardVideoLayer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Storyboards.Drawables; using osuTK; diff --git a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs index 921a039065..b7803f3420 100644 --- a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using NUnit.Framework; diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index 1aa99ceed9..ff670e1232 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Diagnostics; using System.IO; diff --git a/osu.Game/Tests/CleanRunHeadlessGameHost.cs b/osu.Game/Tests/CleanRunHeadlessGameHost.cs index 02d67de5a5..f3c69201e2 100644 --- a/osu.Game/Tests/CleanRunHeadlessGameHost.cs +++ b/osu.Game/Tests/CleanRunHeadlessGameHost.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Runtime.CompilerServices; using osu.Framework; diff --git a/osu.Game/Tests/OsuTestBrowser.cs b/osu.Game/Tests/OsuTestBrowser.cs index 689eae336e..0bc51a0c1e 100644 --- a/osu.Game/Tests/OsuTestBrowser.cs +++ b/osu.Game/Tests/OsuTestBrowser.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Screens; diff --git a/osu.Game/Tests/Visual/DependencyProvidingContainer.cs b/osu.Game/Tests/Visual/DependencyProvidingContainer.cs index acfff4cefe..000509598d 100644 --- a/osu.Game/Tests/Visual/DependencyProvidingContainer.cs +++ b/osu.Game/Tests/Visual/DependencyProvidingContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index 167d5450e9..164faa16aa 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; diff --git a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs index 0570c4e2f2..efd0b80ebf 100644 --- a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.Spectator; diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs index 0f286475bd..88202d4327 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Screens.OnlinePlay; diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs index 12d1846ece..3509519113 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Game.Database; using osu.Game.Online.Rooms; diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs index a9acbdcd7e..975423d19b 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using osu.Framework.Allocation; diff --git a/osu.Game/Tests/Visual/OsuGridTestScene.cs b/osu.Game/Tests/Visual/OsuGridTestScene.cs index 9ef3b2a59d..6ee5593a69 100644 --- a/osu.Game/Tests/Visual/OsuGridTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGridTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index a5e0bddc6b..16496ff320 100644 --- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index 7d382ca1bc..3cca1e59cc 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Tests/Visual/TestReplayPlayer.cs b/osu.Game/Tests/Visual/TestReplayPlayer.cs index bc6dc9bb27..0c9b466152 100644 --- a/osu.Game/Tests/Visual/TestReplayPlayer.cs +++ b/osu.Game/Tests/Visual/TestReplayPlayer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; diff --git a/osu.Game/Tests/VisualTestRunner.cs b/osu.Game/Tests/VisualTestRunner.cs index c8279b9e3c..e04c71d193 100644 --- a/osu.Game/Tests/VisualTestRunner.cs +++ b/osu.Game/Tests/VisualTestRunner.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework; using osu.Framework.Platform; diff --git a/osu.Game/Users/CountryStatistics.cs b/osu.Game/Users/CountryStatistics.cs index 03d455bc04..921d60bb44 100644 --- a/osu.Game/Users/CountryStatistics.cs +++ b/osu.Game/Users/CountryStatistics.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Newtonsoft.Json; namespace osu.Game.Users diff --git a/osu.Game/Users/Drawables/DrawableFlag.cs b/osu.Game/Users/Drawables/DrawableFlag.cs index 929a29251d..289f68ee7f 100644 --- a/osu.Game/Users/Drawables/DrawableFlag.cs +++ b/osu.Game/Users/Drawables/DrawableFlag.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Extensions; diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 0b11d12c46..1761282e2e 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Online.Rooms; diff --git a/osu.Game/Users/UserBrickPanel.cs b/osu.Game/Users/UserBrickPanel.cs index 69b390b36e..b92c9a9afd 100644 --- a/osu.Game/Users/UserBrickPanel.cs +++ b/osu.Game/Users/UserBrickPanel.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; diff --git a/osu.Game/Users/UserGridPanel.cs b/osu.Game/Users/UserGridPanel.cs index 90b6c11f0e..f4ec1475b1 100644 --- a/osu.Game/Users/UserGridPanel.cs +++ b/osu.Game/Users/UserGridPanel.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Users/UserListPanel.cs b/osu.Game/Users/UserListPanel.cs index 3047e70a1a..4942cc7512 100644 --- a/osu.Game/Users/UserListPanel.cs +++ b/osu.Game/Users/UserListPanel.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics.Colour; diff --git a/osu.Game/Users/UserStatus.cs b/osu.Game/Users/UserStatus.cs index 075463c1e0..ffd86b78c7 100644 --- a/osu.Game/Users/UserStatus.cs +++ b/osu.Game/Users/UserStatus.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osuTK.Graphics; using osu.Game.Graphics; diff --git a/osu.Game/Utils/LimitedCapacityQueue.cs b/osu.Game/Utils/LimitedCapacityQueue.cs index 86a106a678..d36aa8af2c 100644 --- a/osu.Game/Utils/LimitedCapacityQueue.cs +++ b/osu.Game/Utils/LimitedCapacityQueue.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections; using System.Collections.Generic; diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index c49e6907ff..502f302157 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using Foundation; using Microsoft.Maui.Devices; From 06565871d684549769ed28b30742f5eed609ea8f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 24 Jun 2023 01:03:18 +0900 Subject: [PATCH 0332/2100] Add flag to disable computing legacy scoring values --- .../Difficulty/CatchDifficultyCalculator.cs | 17 +++++++++++------ .../Difficulty/ManiaDifficultyCalculator.cs | 13 +++++++++---- .../Difficulty/OsuDifficultyCalculator.cs | 17 +++++++++++------ .../Difficulty/TaikoDifficultyCalculator.cs | 17 +++++++++++------ .../Rulesets/Difficulty/DifficultyCalculator.cs | 7 +++++++ 5 files changed, 49 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 36af9fb980..a44aaf6dfa 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -41,18 +41,23 @@ namespace osu.Game.Rulesets.Catch.Difficulty // this is the same as osu!, so there's potential to share the implementation... maybe double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; - CatchScoreV1Processor sv1Processor = new CatchScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); - - return new CatchDifficultyAttributes + CatchDifficultyAttributes attributes = new CatchDifficultyAttributes { StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor, Mods = mods, ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)), - LegacyAccuracyScore = sv1Processor.AccuracyScore, - LegacyComboScore = sv1Processor.ComboScore, - LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio }; + + if (ComputeLegacyScoringValues) + { + CatchScoreV1Processor sv1Processor = new CatchScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); + attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; + attributes.LegacyComboScore = sv1Processor.ComboScore; + attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; + } + + return attributes; } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index d1058a9f8c..675f6099e2 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -48,9 +48,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty HitWindows hitWindows = new ManiaHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); - ManiaScoreV1Processor sv1Processor = new ManiaScoreV1Processor(mods); - - return new ManiaDifficultyAttributes + ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes { StarRating = skills[0].DifficultyValue() * star_scaling_factor, Mods = mods, @@ -58,8 +56,15 @@ namespace osu.Game.Rulesets.Mania.Difficulty // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being. GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate), MaxCombo = beatmap.HitObjects.Sum(maxComboForObject), - LegacyComboScore = sv1Processor.TotalScore }; + + if (ComputeLegacyScoringValues) + { + ManiaScoreV1Processor sv1Processor = new ManiaScoreV1Processor(mods); + attributes.LegacyComboScore = sv1Processor.TotalScore; + } + + return attributes; } private static int maxComboForObject(HitObject hitObject) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 5d6ed4792d..5158ea8a16 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -91,9 +91,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate; - OsuScoreV1Processor sv1Processor = new OsuScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); - - return new OsuDifficultyAttributes + OsuDifficultyAttributes attributes = new OsuDifficultyAttributes { StarRating = starRating, Mods = mods, @@ -109,10 +107,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty HitCircleCount = hitCirclesCount, SliderCount = sliderCount, SpinnerCount = spinnerCount, - LegacyAccuracyScore = sv1Processor.AccuracyScore, - LegacyComboScore = sv1Processor.ComboScore, - LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio }; + + if (ComputeLegacyScoringValues) + { + OsuScoreV1Processor sv1Processor = new OsuScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); + attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; + attributes.LegacyComboScore = sv1Processor.ComboScore; + attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; + } + + return attributes; } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 49222adc89..d2f19e1e67 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -89,9 +89,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty HitWindows hitWindows = new TaikoHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); - TaikoScoreV1Processor sv1Processor = new TaikoScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); - - return new TaikoDifficultyAttributes + TaikoDifficultyAttributes attributes = new TaikoDifficultyAttributes { StarRating = starRating, Mods = mods, @@ -101,10 +99,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty PeakDifficulty = combinedRating, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), - LegacyAccuracyScore = sv1Processor.AccuracyScore, - LegacyComboScore = sv1Processor.ComboScore, - LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio }; + + if (ComputeLegacyScoringValues) + { + TaikoScoreV1Processor sv1Processor = new TaikoScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); + attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; + attributes.LegacyComboScore = sv1Processor.ComboScore; + attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; + } + + return attributes; } /// diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 00c90bd317..d005bbfc7a 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -23,6 +23,13 @@ namespace osu.Game.Rulesets.Difficulty { public abstract class DifficultyCalculator { + /// + /// Whether legacy scoring values (ScoreV1) should be computed to populate the difficulty attributes + /// , , + /// and . + /// + public bool ComputeLegacyScoringValues; + /// /// The beatmap for which difficulty will be calculated. /// From df5b389629e7992ae4f799ed6dbd79a666473ec0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 24 Jun 2023 00:59:36 +0900 Subject: [PATCH 0333/2100] Manual fixes to reduce warnings to zero --- .../ManiaSelectionBlueprintTestScene.cs | 2 +- .../ManiaInputTestScene.cs | 3 ++- .../Edit/ManiaBlueprintContainer.cs | 2 +- .../Preprocessing/OsuDifficultyHitObject.cs | 6 +++--- .../Edit/OsuBlueprintContainer.cs | 2 +- .../Drawables/DrawableStrongNestedHit.cs | 5 ++--- .../TestSceneHitObjectSampleAdjustments.cs | 2 +- .../Screens/TestSceneTeamWinScreen.cs | 2 +- .../Components/DrawableTeamTitleWithHeader.cs | 2 +- .../Components/DrawableTeamWithPlayers.cs | 2 +- osu.Game.Tournament/Models/LadderInfo.cs | 2 +- .../Gameplay/Components/MatchRoundDisplay.cs | 4 ++-- osu.Game/Beatmaps/APIFailTimes.cs | 4 ++-- .../UpdateableBeatmapBackgroundSprite.cs | 2 +- .../Beatmaps/Legacy/LegacyControlPointInfo.cs | 3 --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- osu.Game/Database/IHasFiles.cs | 2 -- .../Graphics/Backgrounds/BeatmapBackground.cs | 2 +- osu.Game/Graphics/ErrorTextFlowContainer.cs | 2 +- .../Graphics/UserInterface/DialogButton.cs | 2 +- .../UserInterfaceV2/OsuHSVColourPicker.cs | 3 +-- .../UserInterfaceV2/OsuHexColourPicker.cs | 3 +-- .../Graphics/UserInterfaceV2/OsuPopover.cs | 3 +-- osu.Game/IO/OsuStorage.cs | 5 +---- .../BeatmapListing/BeatmapSearchFilterRow.cs | 2 -- .../Overlays/BeatmapSet/Buttons/PlayButton.cs | 2 ++ .../BeatmapSet/Buttons/PreviewButton.cs | 2 +- .../Overlays/Changelog/ChangelogListing.cs | 2 +- osu.Game/Overlays/FullscreenOverlay.cs | 2 -- osu.Game/Overlays/OverlayHeader.cs | 3 --- osu.Game/Overlays/OverlaySidebar.cs | 2 -- .../Sections/DebugSettings/GeneralSettings.cs | 2 +- .../Sections/DebugSettings/MemorySettings.cs | 7 ++++--- .../Sections/Input/RulesetBindingsSection.cs | 7 +------ .../StableDirectorySelectScreen.cs | 2 +- osu.Game/Overlays/Settings/SettingsFooter.cs | 4 ++-- osu.Game/Overlays/TabControlOverlayHeader.cs | 3 --- .../Toolbar/ToolbarNotificationButton.cs | 3 +-- .../Edit/Checks/CheckAudioPresence.cs | 2 +- .../Rulesets/Edit/Checks/CheckAudioQuality.cs | 2 +- .../Edit/Checks/CheckBackgroundPresence.cs | 2 +- .../Edit/Checks/CheckBackgroundQuality.cs | 4 ++-- .../Edit/DrawableEditorRulesetWrapper.cs | 3 ++- .../Rulesets/Judgements/JudgementResult.cs | 5 +---- osu.Game/Rulesets/Objects/HitObjectParser.cs | 2 +- .../Objects/Legacy/ConvertHitObjectParser.cs | 1 - .../Rulesets/UI/IPooledHitObjectProvider.cs | 4 +--- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 19 ++++++++----------- osu.Game/Screens/BackgroundScreenStack.cs | 2 +- .../HitObjectOrderedSelectionContainer.cs | 3 ++- .../OnlinePlay/Multiplayer/Multiplayer.cs | 3 ++- .../Spectate/MultiSpectatorPlayerLoader.cs | 3 +-- .../OnlinePlay/OnlinePlaySubScreenStack.cs | 7 +++++-- osu.Game/Screens/OsuScreenStack.cs | 6 +++--- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 3 ++- osu.Game/Screens/Play/HUD/ModDisplay.cs | 4 +--- .../Screens/Play/HUD/UnstableRateCounter.cs | 10 ++++++---- .../Screens/Play/SpectatorResultsScreen.cs | 3 ++- .../Ranking/Statistics/StatisticContainer.cs | 3 +-- .../Ranking/Statistics/StatisticItem.cs | 3 +-- .../Drawables/DrawableStoryboardSprite.cs | 3 ++- 61 files changed, 89 insertions(+), 118 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs index 281dec3c79..80e1b753ea 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { protected override Container Content => blueprints ?? base.Content; - private readonly Container blueprints; + private readonly Container? blueprints; [Cached(typeof(Playfield))] public Playfield Playfield { get; } diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs index ec249f6ae9..62591ce4ca 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs @@ -12,7 +12,8 @@ namespace osu.Game.Rulesets.Mania.Tests { public abstract partial class ManiaInputTestScene : OsuTestScene { - private readonly Container content; + private readonly Container? content; + protected override Container Content => content ?? base.Content; protected ManiaInputTestScene(int keys) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs index fb3e2d494e..d0eb8c1e6e 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Edit { } - public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) + public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject) { switch (hitObject) { diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 5215920ea0..e627c9ad67 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -82,13 +82,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing /// public double HitWindowGreat { get; private set; } - private readonly OsuHitObject lastLastObject; + private readonly OsuHitObject? lastLastObject; private readonly OsuHitObject lastObject; - public OsuDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, List objects, int index) + public OsuDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject? lastLastObject, double clockRate, List objects, int index) : base(hitObject, lastObject, clockRate, objects, index) { - this.lastLastObject = (OsuHitObject)lastLastObject; + this.lastLastObject = lastLastObject as OsuHitObject; this.lastObject = (OsuHitObject)lastObject; // Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects. diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs index ed149d004c..54c54fca17 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Edit protected override SelectionHandler CreateSelectionHandler() => new OsuSelectionHandler(); - public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) + public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject) { switch (hitObject) { diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index 6ee29fa014..9b410d1871 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs @@ -1,7 +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 JetBrains.Annotations; using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -11,9 +10,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// public abstract partial class DrawableStrongNestedHit : DrawableTaikoHitObject { - public new DrawableTaikoHitObject ParentHitObject => (DrawableTaikoHitObject)base.ParentHitObject; + public new DrawableTaikoHitObject? ParentHitObject => base.ParentHitObject as DrawableTaikoHitObject; - protected DrawableStrongNestedHit([CanBeNull] StrongNestedHitObject nestedHit) + protected DrawableStrongNestedHit(StrongNestedHitObject? nestedHit) : base(nestedHit) { } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index 29de0bff79..1415ff4b0f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -358,7 +358,7 @@ namespace osu.Game.Tests.Visual.Editing var popover = this.ChildrenOfType().SingleOrDefault(); var textBox = popover?.ChildrenOfType().First(); - return textBox?.Current.Value == bank && string.IsNullOrEmpty(textBox?.PlaceholderText.ToString()); + return textBox?.Current.Value == bank && string.IsNullOrEmpty(textBox.PlaceholderText.ToString()); }); private void samplePopoverHasIndeterminateBank() => AddUntilStep("sample popover has indeterminate bank", () => diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs index 8096988864..2ed87cdcd5 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs @@ -15,7 +15,7 @@ namespace osu.Game.Tournament.Tests.Screens { AddStep("set up match", () => { - var match = Ladder.CurrentMatch.Value; + var match = Ladder.CurrentMatch.Value!; match.Round.Value = Ladder.Rounds.FirstOrDefault(g => g.Name.Value == "Finals"); match.Completed.Value = true; diff --git a/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs index 59e261a7dd..89f45fc1d3 100644 --- a/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs +++ b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs @@ -10,7 +10,7 @@ namespace osu.Game.Tournament.Components { public partial class DrawableTeamTitleWithHeader : CompositeDrawable { - public DrawableTeamTitleWithHeader(TournamentTeam team, TeamColour colour) + public DrawableTeamTitleWithHeader(TournamentTeam? team, TeamColour colour) { AutoSizeAxes = Axes.Both; diff --git a/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs index 5ebed34e6a..4f0c7d6b72 100644 --- a/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs +++ b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs @@ -13,7 +13,7 @@ namespace osu.Game.Tournament.Components { public partial class DrawableTeamWithPlayers : CompositeDrawable { - public DrawableTeamWithPlayers(TournamentTeam team, TeamColour colour) + public DrawableTeamWithPlayers(TournamentTeam? team, TeamColour colour) { AutoSizeAxes = Axes.Both; diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs index b5bc5fd307..3defd517cd 100644 --- a/osu.Game.Tournament/Models/LadderInfo.cs +++ b/osu.Game.Tournament/Models/LadderInfo.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tournament.Models public List Progressions = new List(); [JsonIgnore] // updated manually in TournamentGameBase - public Bindable CurrentMatch = new Bindable(); + public Bindable CurrentMatch = new Bindable(); public Bindable ChromaKeyWidth = new BindableInt(1024) { diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs index 79de4e465e..bd23317e1f 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs @@ -10,7 +10,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { public partial class MatchRoundDisplay : TournamentSpriteTextWithBackground { - private readonly Bindable currentMatch = new Bindable(); + private readonly Bindable currentMatch = new Bindable(); [BackgroundDependencyLoader] private void load(LadderInfo ladder) @@ -19,7 +19,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components currentMatch.BindTo(ladder.CurrentMatch); } - private void matchChanged(ValueChangedEvent match) => + private void matchChanged(ValueChangedEvent match) => Text.Text = match.NewValue?.Round.Value?.Name.Value ?? "Unknown Round"; } } diff --git a/osu.Game/Beatmaps/APIFailTimes.cs b/osu.Game/Beatmaps/APIFailTimes.cs index 7218906b38..09ab16598d 100644 --- a/osu.Game/Beatmaps/APIFailTimes.cs +++ b/osu.Game/Beatmaps/APIFailTimes.cs @@ -15,12 +15,12 @@ namespace osu.Game.Beatmaps /// Points of failure on a relative time scale (usually 0..100). /// [JsonProperty(@"fail")] - public int[] Fails { get; set; } = Array.Empty(); + public int[]? Fails { get; set; } = Array.Empty(); /// /// Points of retry on a relative time scale (usually 0..100). /// [JsonProperty(@"exit")] - public int[] Retries { get; set; } = Array.Empty(); + public int[]? Retries { get; set; } = Array.Empty(); } } diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs index 72f37143d0..67eedf655e 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs @@ -50,7 +50,7 @@ namespace osu.Game.Beatmaps.Drawables return drawable; } - private Drawable getDrawableForModel(IBeatmapInfo model) + private Drawable getDrawableForModel(IBeatmapInfo? model) { // prefer online cover where available. if (model?.BeatmapSet is IBeatmapSetOnlineInfo online) diff --git a/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs b/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs index fc80f0db6f..6dda18bc4d 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Lists; using osu.Game.Beatmaps.ControlPoints; @@ -24,7 +23,6 @@ namespace osu.Game.Beatmaps.Legacy /// /// The time to find the sound control point at. /// The sound control point. - [NotNull] public SampleControlPoint SamplePointAt(double time) => BinarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT); /// @@ -40,7 +38,6 @@ namespace osu.Game.Beatmaps.Legacy /// /// The time to find the difficulty control point at. /// The difficulty control point. - [NotNull] public DifficultyControlPoint DifficultyPointAt(double time) => BinarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT); public override void Clear() diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 309a5818ae..ba555a7926 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -257,7 +257,7 @@ namespace osu.Game.Configuration string skinName = string.Empty; if (Guid.TryParse(skin, out var id)) - skinName = LookupSkinName(id) ?? string.Empty; + skinName = LookupSkinName(id); return new SettingDescription( rawValue: skinName, diff --git a/osu.Game/Database/IHasFiles.cs b/osu.Game/Database/IHasFiles.cs index 3f6531832f..d64ac9b662 100644 --- a/osu.Game/Database/IHasFiles.cs +++ b/osu.Game/Database/IHasFiles.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using JetBrains.Annotations; namespace osu.Game.Database { @@ -13,7 +12,6 @@ namespace osu.Game.Database public interface IHasFiles where TFile : INamedFileInfo { - [NotNull] List Files { get; } string Hash { get; set; } diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs index beb7c5a4df..b8de0f7f1e 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs @@ -22,7 +22,7 @@ namespace osu.Game.Graphics.Backgrounds [BackgroundDependencyLoader] private void load(LargeTextureStore textures) { - Sprite.Texture = Beatmap?.GetBackground() ?? textures.Get(fallbackTextureName); + Sprite.Texture = Beatmap.GetBackground() ?? textures.Get(fallbackTextureName); } public override bool Equals(Background other) diff --git a/osu.Game/Graphics/ErrorTextFlowContainer.cs b/osu.Game/Graphics/ErrorTextFlowContainer.cs index 7386baf83f..40c7580647 100644 --- a/osu.Game/Graphics/ErrorTextFlowContainer.cs +++ b/osu.Game/Graphics/ErrorTextFlowContainer.cs @@ -23,7 +23,7 @@ namespace osu.Game.Graphics RemovePart(textPart); } - public void AddErrors(string[] errors) + public void AddErrors(string[]? errors) { ClearErrors(); diff --git a/osu.Game/Graphics/UserInterface/DialogButton.cs b/osu.Game/Graphics/UserInterface/DialogButton.cs index 06ebe48850..db81bc991d 100644 --- a/osu.Game/Graphics/UserInterface/DialogButton.cs +++ b/osu.Game/Graphics/UserInterface/DialogButton.cs @@ -28,7 +28,7 @@ namespace osu.Game.Graphics.UserInterface private const float hover_duration = 500; private const float click_duration = 200; - public event Action StateChanged; + public event Action? StateChanged; private SelectionState state; diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs b/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs index 75ff1e5665..63bad283a8 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs @@ -1,7 +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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -24,7 +23,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override SaturationValueSelector CreateSaturationValueSelector() => new OsuSaturationValueSelector(); [BackgroundDependencyLoader(true)] - private void load([CanBeNull] OverlayColourProvider colourProvider, OsuColour osuColour) + private void load(OverlayColourProvider? colourProvider, OsuColour osuColour) { Background.Colour = colourProvider?.Dark5 ?? osuColour.GreySeaFoamDark; diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs b/osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs index 6633ba0eb2..3621ca165f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs @@ -1,7 +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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -21,7 +20,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 } [BackgroundDependencyLoader(true)] - private void load([CanBeNull] OverlayColourProvider overlayColourProvider, OsuColour osuColour) + private void load(OverlayColourProvider? overlayColourProvider, OsuColour osuColour) { Background.Colour = overlayColourProvider?.Dark6 ?? osuColour.GreySeaFoamDarker; } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs index 9153d5253d..00e5b8838c 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs @@ -1,7 +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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; @@ -39,7 +38,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 } [BackgroundDependencyLoader(true)] - private void load([CanBeNull] OverlayColourProvider colourProvider, OsuColour colours) + private void load(OverlayColourProvider? colourProvider, OsuColour colours) { Background.Colour = Arrow.Colour = colourProvider?.Background4 ?? colours.GreySeaFoamDarker; } diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index a2b89b6d97..a936fa74da 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Configuration; @@ -20,13 +19,11 @@ namespace osu.Game.IO /// /// The custom storage path as selected by the user. /// - [CanBeNull] - public string CustomStoragePath => storageConfig.Get(StorageConfig.FullPath); + public string? CustomStoragePath => storageConfig.Get(StorageConfig.FullPath); /// /// The default storage path to be used if a custom storage path hasn't been selected or is not accessible. /// - [NotNull] public string DefaultStoragePath => defaultStorage.GetFullPath("."); private readonly GameHost host; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs index 608ecbb6f3..6d75521cb0 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs @@ -1,7 +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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -64,7 +63,6 @@ namespace osu.Game.Overlays.BeatmapListing Current = filterWithValue.Current; } - [NotNull] protected virtual Drawable CreateFilter() => new BeatmapSearchFilter(); protected partial class BeatmapSearchFilter : TabControl diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs index c43be33290..5f9cdf5065 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs @@ -3,6 +3,7 @@ #nullable disable +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -24,6 +25,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons private readonly BindableBool playing = new BindableBool(); + [CanBeNull] public PreviewTrack Preview { get; private set; } private APIBeatmapSet beatmapSet; diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs index 62a8bf80d3..2254514a44 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons private readonly Box background, progress; private readonly PlayButton playButton; - private PreviewTrack preview => playButton.Preview; + private PreviewTrack? preview => playButton.Preview; public IBindable Playing => playButton.Playing; diff --git a/osu.Game/Overlays/Changelog/ChangelogListing.cs b/osu.Game/Overlays/Changelog/ChangelogListing.cs index 3a648f66cf..c4320dcbd0 100644 --- a/osu.Game/Overlays/Changelog/ChangelogListing.cs +++ b/osu.Game/Overlays/Changelog/ChangelogListing.cs @@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Changelog { public partial class ChangelogListing : ChangelogContent { - private readonly List entries; + private readonly List? entries; public ChangelogListing(List entries) { diff --git a/osu.Game/Overlays/FullscreenOverlay.cs b/osu.Game/Overlays/FullscreenOverlay.cs index b58a3b929b..7ae5167081 100644 --- a/osu.Game/Overlays/FullscreenOverlay.cs +++ b/osu.Game/Overlays/FullscreenOverlay.cs @@ -1,7 +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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -81,7 +80,6 @@ namespace osu.Game.Overlays Waves.FourthWaveColour = ColourProvider.Dark3; } - [NotNull] protected abstract T CreateHeader(); public override void Show() diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs index 827a7749af..3d71b7d5ae 100644 --- a/osu.Game/Overlays/OverlayHeader.cs +++ b/osu.Game/Overlays/OverlayHeader.cs @@ -1,7 +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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -96,10 +95,8 @@ namespace osu.Game.Overlays titleBackground.Colour = colourProvider.Dark5; } - [NotNull] protected virtual Drawable CreateContent() => Empty(); - [NotNull] protected virtual Drawable CreateBackground() => Empty(); protected abstract OverlayTitle CreateTitle(); diff --git a/osu.Game/Overlays/OverlaySidebar.cs b/osu.Game/Overlays/OverlaySidebar.cs index b08a9d08a4..f1bdfbddac 100644 --- a/osu.Game/Overlays/OverlaySidebar.cs +++ b/osu.Game/Overlays/OverlaySidebar.cs @@ -1,7 +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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -71,7 +70,6 @@ namespace osu.Game.Overlays scrollbarBackground.Colour = colourProvider.Background3; } - [NotNull] protected virtual Drawable CreateContent() => Empty(); private partial class SidebarScrollContainer : OsuScrollContainer diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs index 1044810bdc..01408ca087 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings protected override LocalisableString Header => DebugSettingsStrings.GeneralHeader; [BackgroundDependencyLoader(true)] - private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig, IPerformFromScreenRunner performer) + private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig, IPerformFromScreenRunner? performer) { Children = new Drawable[] { diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs index 0d2d163859..d5de7ae2db 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs @@ -5,6 +5,7 @@ using System; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Framework.Logging; @@ -56,7 +57,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings { try { - var token = realm.BlockAllOperations("maintenance"); + IDisposable? token = realm.BlockAllOperations("maintenance"); blockAction.Enabled.Value = false; @@ -73,10 +74,10 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings void unblock() { - if (token == null) + if (token.IsNull()) return; - token?.Dispose(); + token.Dispose(); token = null; Scheduler.Add(() => diff --git a/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs b/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs index 0ed7de9d3f..3b5002b423 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs @@ -2,19 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; -using osu.Game.Graphics; using osu.Game.Rulesets; namespace osu.Game.Overlays.Settings.Sections.Input { public partial class RulesetBindingsSection : SettingsSection { - public override Drawable CreateIcon() => ruleset?.CreateInstance().CreateIcon() ?? new SpriteIcon - { - Icon = OsuIcon.Hot - }; + public override Drawable CreateIcon() => ruleset.CreateInstance().CreateIcon(); public override LocalisableString Header => ruleset.Name; diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.cs index 1f62077f20..1b935b0cec 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.cs @@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled; - protected override bool IsValidDirectory(DirectoryInfo info) => info?.GetFiles("osu!.*.cfg").Any() ?? false; + protected override bool IsValidDirectory(DirectoryInfo? info) => info?.GetFiles("osu!.*.cfg").Any() ?? false; public override LocalisableString HeaderText => "Please select your osu!stable install location"; diff --git a/osu.Game/Overlays/Settings/SettingsFooter.cs b/osu.Game/Overlays/Settings/SettingsFooter.cs index ffb955f3bd..4e9d4c0d28 100644 --- a/osu.Game/Overlays/Settings/SettingsFooter.cs +++ b/osu.Game/Overlays/Settings/SettingsFooter.cs @@ -92,8 +92,8 @@ namespace osu.Game.Overlays.Settings Height = 20; } - [BackgroundDependencyLoader(true)] - private void load(ChangelogOverlay changelog) + [BackgroundDependencyLoader] + private void load(ChangelogOverlay? changelog) { Action = () => changelog?.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version); diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs index edfe38b2da..3875f18152 100644 --- a/osu.Game/Overlays/TabControlOverlayHeader.cs +++ b/osu.Game/Overlays/TabControlOverlayHeader.cs @@ -1,7 +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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -83,13 +82,11 @@ namespace osu.Game.Overlays controlBackground.Colour = colourProvider.Dark4; } - [NotNull] protected virtual OsuTabControl CreateTabControl() => new OverlayHeaderTabControl(); /// /// Creates a on the opposite side of the . Used mostly to create . /// - [NotNull] protected virtual Drawable CreateTabControlContent() => Empty(); public partial class OverlayHeaderTabControl : OverlayTabControl diff --git a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs index 1871371750..9971871229 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs @@ -41,8 +41,7 @@ namespace osu.Game.Overlays.Toolbar { StateContainer = notificationOverlay as NotificationOverlay; - if (notificationOverlay != null) - NotificationCount.BindTo(notificationOverlay.UnreadCount); + NotificationCount.BindTo(notificationOverlay.UnreadCount); NotificationCount.ValueChanged += count => { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioPresence.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioPresence.cs index e922ddf023..416a0d5897 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAudioPresence.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioPresence.cs @@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Edit.Checks { protected override CheckCategory Category => CheckCategory.Audio; protected override string TypeOfFile => "audio"; - protected override string? GetFilename(IBeatmap beatmap) => beatmap.Metadata?.AudioFile; + protected override string GetFilename(IBeatmap beatmap) => beatmap.Metadata.AudioFile; } } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs index daa33fb0da..440d4e8e62 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Edit.Checks public IEnumerable Run(BeatmapVerifierContext context) { - string? audioFile = context.Beatmap.Metadata?.AudioFile; + string audioFile = context.Beatmap.Metadata.AudioFile; if (string.IsNullOrEmpty(audioFile)) yield break; diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundPresence.cs b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundPresence.cs index 4ca93a9807..04cbba1e8c 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundPresence.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundPresence.cs @@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Edit.Checks { protected override CheckCategory Category => CheckCategory.Resources; protected override string TypeOfFile => "background"; - protected override string? GetFilename(IBeatmap beatmap) => beatmap.Metadata?.BackgroundFile; + protected override string GetFilename(IBeatmap beatmap) => beatmap.Metadata.BackgroundFile; } } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs index 8c3a5c026d..5008c13d9a 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs @@ -33,8 +33,8 @@ namespace osu.Game.Rulesets.Edit.Checks public IEnumerable Run(BeatmapVerifierContext context) { - string? backgroundFile = context.Beatmap.Metadata?.BackgroundFile; - if (backgroundFile == null) + string backgroundFile = context.Beatmap.Metadata.BackgroundFile; + if (string.IsNullOrEmpty(backgroundFile)) yield break; var texture = context.WorkingBeatmap.GetBackground(); diff --git a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs index 324f2068e9..174b278d89 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs @@ -3,6 +3,7 @@ using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mods; @@ -92,7 +93,7 @@ namespace osu.Game.Rulesets.Edit { base.Dispose(isDisposing); - if (beatmap != null) + if (beatmap.IsNotNull()) { beatmap.HitObjectAdded -= addHitObject; beatmap.HitObjectRemoved -= removeHitObject; diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs index f001a4cd92..c67f8b9fd5 100644 --- a/osu.Game/Rulesets/Judgements/JudgementResult.cs +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using JetBrains.Annotations; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; @@ -22,13 +21,11 @@ namespace osu.Game.Rulesets.Judgements /// /// The which was judged. /// - [NotNull] public readonly HitObject HitObject; /// /// The which this applies for. /// - [NotNull] public readonly Judgement Judgement; /// @@ -97,7 +94,7 @@ namespace osu.Game.Rulesets.Judgements /// /// The which was judged. /// The to refer to for scoring information. - public JudgementResult([NotNull] HitObject hitObject, [NotNull] Judgement judgement) + public JudgementResult(HitObject hitObject, Judgement judgement) { HitObject = hitObject; Judgement = judgement; diff --git a/osu.Game/Rulesets/Objects/HitObjectParser.cs b/osu.Game/Rulesets/Objects/HitObjectParser.cs index d3c29d90ce..c6e250bd74 100644 --- a/osu.Game/Rulesets/Objects/HitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/HitObjectParser.cs @@ -5,6 +5,6 @@ namespace osu.Game.Rulesets.Objects { public abstract class HitObjectParser { - public abstract HitObject Parse(string text); + public abstract HitObject? Parse(string text); } } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 8eda2a8f61..3dbe7b6519 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -44,7 +44,6 @@ namespace osu.Game.Rulesets.Objects.Legacy FormatVersion = formatVersion; } - [CanBeNull] public override HitObject Parse(string text) { string[] split = text.Split(','); diff --git a/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs b/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs index f5739ee525..01c8e6d1da 100644 --- a/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs +++ b/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs @@ -1,7 +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 JetBrains.Annotations; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -15,7 +14,6 @@ namespace osu.Game.Rulesets.UI /// The to retrieve the representation of. /// The parenting , if any. /// The representing , or null if no poolable representation exists. - [CanBeNull] - DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject, [CanBeNull] DrawableHitObject parent); + DrawableHitObject? GetPooledDrawableRepresentation(HitObject hitObject, DrawableHitObject? parent); } } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 87e1e79f87..2a54ef16c6 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -126,19 +126,16 @@ namespace osu.Game.Scoring.Legacy // As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing. double offset = beatmap?.BeatmapInfo.BeatmapVersion < 5 ? -LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0; - if (score.Replay != null) + int lastTime = 0; + + foreach (var f in score.Replay.Frames) { - int lastTime = 0; + var legacyFrame = getLegacyFrame(f); - foreach (var f in score.Replay.Frames) - { - var legacyFrame = getLegacyFrame(f); - - // Rounding because stable could only parse integral values - int time = (int)Math.Round(legacyFrame.Time + offset); - replayData.Append(FormattableString.Invariant($"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState},")); - lastTime = time; - } + // Rounding because stable could only parse integral values + int time = (int)Math.Round(legacyFrame.Time + offset); + replayData.Append(FormattableString.Invariant($"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState},")); + lastTime = time; } // Warning: this is purposefully hardcoded as a string rather than interpolating, as in some cultures the minus sign is not encoded as the standard ASCII U+00C2 codepoint, diff --git a/osu.Game/Screens/BackgroundScreenStack.cs b/osu.Game/Screens/BackgroundScreenStack.cs index 6ebc97ebbb..99ca383b9f 100644 --- a/osu.Game/Screens/BackgroundScreenStack.cs +++ b/osu.Game/Screens/BackgroundScreenStack.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens /// /// The screen to attempt to push. /// Whether the push succeeded. For example, if the existing screen was already of the correct type this will return false. - public bool Push(BackgroundScreen screen) + public bool Push(BackgroundScreen? screen) { if (screen == null) return false; diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs index 0edaaf9825..8f54d55d5d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Edit; @@ -67,7 +68,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.Dispose(isDisposing); - if (editorBeatmap != null) + if (editorBeatmap.IsNotNull()) editorBeatmap.BeatmapReprocessed -= SortInternal; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 66ae814444..514b80b999 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.Multiplayer; @@ -93,7 +94,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.Dispose(isDisposing); - if (client != null) + if (client.IsNotNull()) client.RoomUpdated -= onRoomUpdated; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs index 9a43e96a50..737f301f4d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Game.Scoring; using osu.Game.Screens.Menu; @@ -15,7 +14,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public partial class MultiSpectatorPlayerLoader : SpectatorPlayerLoader { - public MultiSpectatorPlayerLoader([NotNull] Score score, [NotNull] Func createPlayer) + public MultiSpectatorPlayerLoader(Score score, Func createPlayer) : base(score, createPlayer) { } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs index 2e3e2e1dc0..6695c97508 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs @@ -1,18 +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.Diagnostics; using osu.Framework.Screens; namespace osu.Game.Screens.OnlinePlay { public partial class OnlinePlaySubScreenStack : OsuScreenStack { - protected override void ScreenChanged(IScreen prev, IScreen next) + protected override void ScreenChanged(IScreen prev, IScreen? next) { base.ScreenChanged(prev, next); // because this is a screen stack within a screen stack, let's manually handle disabled changes to simplify things. - var osuScreen = ((OsuScreen)next); + var osuScreen = next as OsuScreen; + + Debug.Assert(osuScreen != null); bool disallowChanges = osuScreen.DisallowExternalBeatmapRulesetChanges; diff --git a/osu.Game/Screens/OsuScreenStack.cs b/osu.Game/Screens/OsuScreenStack.cs index ef579fac85..7d1f6419ad 100644 --- a/osu.Game/Screens/OsuScreenStack.cs +++ b/osu.Game/Screens/OsuScreenStack.cs @@ -52,12 +52,12 @@ namespace osu.Game.Screens ScreenChanged(prev, next); } - protected virtual void ScreenChanged(IScreen prev, IScreen next) + protected virtual void ScreenChanged(IScreen prev, IScreen? next) { setParallax(next); } - private void setParallax(IScreen next) => - parallaxContainer.ParallaxAmount = ParallaxContainer.DEFAULT_PARALLAX_AMOUNT * (((IOsuScreen)next)?.BackgroundParallaxAmount ?? 1.0f); + private void setParallax(IScreen? next) => + parallaxContainer.ParallaxAmount = ParallaxContainer.DEFAULT_PARALLAX_AMOUNT * ((next as IOsuScreen)?.BackgroundParallaxAmount ?? 1.0f); } } diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index 19ede5533f..9fdd735804 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Judgements; @@ -59,7 +60,7 @@ namespace osu.Game.Screens.Play.HUD { base.Dispose(isDisposing); - if (HealthProcessor != null) + if (HealthProcessor.IsNotNull()) HealthProcessor.NewJudgement -= onNewJudgement; } } diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 579a0d163a..c064cdb040 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Play.HUD public ExpansionMode ExpansionMode = ExpansionMode.ExpandOnHover; - private readonly BindableWithCurrent> current = new BindableWithCurrent>(); + private readonly BindableWithCurrent> current = new BindableWithCurrent>(Array.Empty()); public Bindable> Current { @@ -63,8 +63,6 @@ namespace osu.Game.Screens.Play.HUD { iconsContainer.Clear(); - if (mods.NewValue == null) return; - foreach (Mod mod in mods.NewValue) iconsContainer.Add(new ModIcon(mod) { Scale = new Vector2(0.6f) }); diff --git a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs index e3034b2442..701b8a8732 100644 --- a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs +++ b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -75,10 +76,11 @@ namespace osu.Game.Screens.Play.HUD { base.Dispose(isDisposing); - if (scoreProcessor == null) return; - - scoreProcessor.NewJudgement -= updateDisplay; - scoreProcessor.JudgementReverted -= updateDisplay; + if (scoreProcessor.IsNotNull()) + { + scoreProcessor.NewJudgement -= updateDisplay; + scoreProcessor.JudgementReverted -= updateDisplay; + } } private partial class TextComponent : CompositeDrawable, IHasText diff --git a/osu.Game/Screens/Play/SpectatorResultsScreen.cs b/osu.Game/Screens/Play/SpectatorResultsScreen.cs index 67ec1373df..001d3b4bbc 100644 --- a/osu.Game/Screens/Play/SpectatorResultsScreen.cs +++ b/osu.Game/Screens/Play/SpectatorResultsScreen.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Screens; using osu.Game.Online.Spectator; using osu.Game.Scoring; @@ -40,7 +41,7 @@ namespace osu.Game.Screens.Play { base.Dispose(isDisposing); - if (spectatorClient != null) + if (spectatorClient.IsNotNull()) spectatorClient.OnUserBeganPlaying -= userBeganPlaying; } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index 4202b2158e..9191ee6f52 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -1,7 +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.Diagnostics.CodeAnalysis; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -22,7 +21,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// Creates a new . /// /// The to display. - public StatisticContainer([NotNull] StatisticItem item) + public StatisticContainer(StatisticItem item) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index 6a595bf05c..fd7a0ddb4f 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -34,7 +33,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// The name of the item. Can be to hide the item header. /// 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. - public StatisticItem(LocalisableString name, [NotNull] Func createContent, bool requiresHitEvents = false) + public StatisticItem(LocalisableString name, Func createContent, bool requiresHitEvents = false) { Name = name; RequiresHitEvents = requiresHitEvents; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 63f644886a..ec0cb7ca19 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; @@ -106,7 +107,7 @@ namespace osu.Game.Storyboards.Drawables { base.Dispose(isDisposing); - if (skin != null) + if (skin.IsNotNull()) skin.SourceChanged -= skinSourceChanged; } } From ad6650cbfa229c61910ff59114a740f7cc9fb3d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 24 Jun 2023 01:56:16 +0900 Subject: [PATCH 0334/2100] Add automated commit to blame ignore revs --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index b85862270b..d35d4be412 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -6,3 +6,5 @@ 212d78865a6b5f091173a347bad5686834d1d5fe # Add partial specs in mobile projects too 00c11b2b4e389e48f3995d63484a6bc66a7afbdb +# Mass NRT enabling +0ab0c52ad577b3e7b406d09fa6056a56ff997c3e From 4a2f259f7e254c57d33f1829f9fcf358207f10e5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 23 Jun 2023 22:04:29 +0300 Subject: [PATCH 0335/2100] Add test coverage for tournament players with profile colours --- .../TestSceneTournamentMatchChatDisplay.cs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs index b552d49d1d..8003011475 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs @@ -43,6 +43,12 @@ namespace osu.Game.Tournament.Tests.Components OnlineID = 4, }; + private readonly TournamentUser blueUserWithCustomColour = new TournamentUser + { + Username = "nekodex", + OnlineID = 5, + }; + [Cached] private LadderInfo ladderInfo = new LadderInfo(); @@ -67,7 +73,7 @@ namespace osu.Game.Tournament.Tests.Components }, Team2 = { - Value = new TournamentTeam { Players = new BindableList { blueUser } } + Value = new TournamentTeam { Players = new BindableList { blueUser, blueUserWithCustomColour } } } }; @@ -108,6 +114,21 @@ namespace osu.Game.Tournament.Tests.Components AddUntilStep("message from team blue is blue color", () => this.ChildrenOfType().Last().AccentColour, () => Is.EqualTo(TournamentGame.COLOUR_BLUE)); + var userWithCustomColour = blueUserWithCustomColour.ToAPIUser(); + userWithCustomColour.Colour = "#e45678"; + + AddStep("message from team blue with custom colour", () => testChannel.AddNewMessages(new Message(nextMessageId()) + { + Sender = userWithCustomColour, + Content = "Not on my watch. Prepare to eat saaaaaaaaaand. Lots and lots of saaaaaaand." + })); + + AddUntilStep("message from team blue is blue color", () => + this.ChildrenOfType().Last().AccentColour, () => Is.EqualTo(TournamentGame.COLOUR_BLUE)); + + AddUntilStep("message from user with custom colour is inverted", () => + this.ChildrenOfType().Last().Inverted, () => Is.EqualTo(true)); + AddStep("message from admin", () => testChannel.AddNewMessages(new Message(nextMessageId()) { Sender = admin, From 7a771609f90b26d3bce25fe934ccef40d8d7330e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 23 Jun 2023 22:20:25 +0300 Subject: [PATCH 0336/2100] Reword and fix typo --- osu.Game/Overlays/Chat/ChatLine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index fdf91dce23..bbc3ee5bf4 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -70,7 +70,7 @@ namespace osu.Game.Overlays.Chat private Container? highlight; /// - /// The colour to use to paint the chat mesasge author's username. + /// The colour used to paint the author's username. /// /// /// The colour can be set explicitly by consumers via the property initialiser. From ff17685bc3650928062fecb20fe9b31698c0d839 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 23 Jun 2023 22:37:44 +0300 Subject: [PATCH 0337/2100] Fix `OpenUserProfile` links having multiple argument types --- osu.Game/Graphics/Containers/LinkFlowContainer.cs | 8 +++++++- osu.Game/OsuGame.cs | 10 +++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index 2d27ce906b..a2c9b0ac32 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -74,7 +74,13 @@ namespace osu.Game.Graphics.Containers } public void AddUserLink(IUser user, Action creationParameters = null) - => createLink(CreateChunkFor(user.Username, true, CreateSpriteText, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user), "view profile"); + { + string argument = user.OnlineID > 1 + ? user.OnlineID.ToString() + : user.Username; + + createLink(CreateChunkFor(user.Username, true, CreateSpriteText, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, argument), "view profile"); + } private void createLink(ITextPart textPart, LinkDetails link, LocalisableString tooltipText, Action action = null) { diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a80639d4ff..2db8872524 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -446,15 +446,11 @@ namespace osu.Game break; case LinkAction.OpenUserProfile: - if (!(link.Argument is IUser user)) - { - user = int.TryParse(argString, out int userId) - ? new APIUser { Id = userId } - : new APIUser { Username = argString }; - } + var user = int.TryParse(argString, out int userId) + ? new APIUser { Id = userId } + : new APIUser { Username = argString }; ShowUser(user); - break; case LinkAction.OpenWiki: From 3585c3f1d5b96e66aec5aa9162fd75de2b6eb336 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Jun 2023 15:58:05 +0900 Subject: [PATCH 0338/2100] Apply required nullability changes --- .../Editor/TestSceneSliderSplitting.cs | 2 +- osu.Game.Rulesets.Taiko/Skinning/Default/DefaultInputDrum.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs | 2 +- osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 4 ++++ .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 4 ++-- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs index 605771fb20..8ba97892fe 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs @@ -244,7 +244,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor MenuItem? item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText); - item?.Action?.Value(); + item?.Action.Value?.Invoke(); }); } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultInputDrum.cs index 6d19db999c..60bacf6413 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultInputDrum.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultInputDrum.cs @@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default back = rim; } - if (target != null) + if (target != null && back != null) { const float scale_amount = 0.05f; const float alpha_amount = 0.5f; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index f97019e466..c6dad1b25e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("attempt seek", () => { - InputManager.MoveMouseTo(getSongProgress()); + InputManager.MoveMouseTo(getSongProgress().AsNonNull()); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index f094d40caa..af3a6e178c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -825,6 +825,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Click on a filtered difficulty", () => { + Debug.Assert(filteredIcon != null); + InputManager.MoveMouseTo(filteredIcon); InputManager.Click(MouseButton.Left); @@ -918,6 +920,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Click on a difficulty", () => { + Debug.Assert(difficultyIcon != null); + InputManager.MoveMouseTo(difficultyIcon); InputManager.Click(MouseButton.Left); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index d566a04261..dcb1f730a2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -218,7 +218,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("dismiss mod customisation via toggle", () => { - InputManager.MoveMouseTo(modSelectOverlay.CustomisationButton); + InputManager.MoveMouseTo(modSelectOverlay.CustomisationButton.AsNonNull()); InputManager.Click(MouseButton.Left); }); assertCustomisationToggleState(disabled: false, active: false); @@ -558,7 +558,7 @@ namespace osu.Game.Tests.Visual.UserInterface void navigateAndClick() where T : Drawable { - InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().FirstOrDefault()); + InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().First()); InputManager.Click(MouseButton.Left); } } From 58e6b3782b63e3c16ed1e4ae056f2ec5bb2222f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 24 Jun 2023 09:48:16 +0900 Subject: [PATCH 0339/2100] Fix a couple of remaining issues --- .../Visual/Online/TestSceneCurrentlyPlayingDisplay.cs | 2 +- osu.Game/Rulesets/RulesetStore.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs index 885c00be80..5237238f63 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online public void TestBasicDisplay() { AddStep("Add playing user", () => spectatorClient.SendStartPlay(streamingUser.Id, 0)); - AddUntilStep("Panel loaded", () => currentlyPlaying.ChildrenOfType()?.FirstOrDefault()?.User.Id == 2); + AddUntilStep("Panel loaded", () => currentlyPlaying.ChildrenOfType().FirstOrDefault()?.User.Id == 2); AddStep("Remove playing user", () => spectatorClient.SendEndPlay(streamingUser.Id)); AddUntilStep("Panel no longer present", () => !currentlyPlaying.ChildrenOfType().Any()); } diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 881b09bd1b..ac36ee6494 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Reflection; using osu.Framework; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Logging; using osu.Framework.Platform; @@ -32,7 +33,7 @@ namespace osu.Game.Rulesets // This null check prevents Android from attempting to load the rulesets from disk, // as the underlying path "AppContext.BaseDirectory", despite being non-nullable, it returns null on android. // See https://github.com/xamarin/xamarin-android/issues/3489. - if (RuntimeInfo.StartupDirectory != null) + if (RuntimeInfo.StartupDirectory.IsNotNull()) loadFromDisk(); // the event handler contains code for resolving dependency on the game assembly for rulesets located outside the base game directory. From 354e85a2e15c081f8742d29c4880f862b2d85fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 24 Jun 2023 14:35:23 +0200 Subject: [PATCH 0340/2100] Trim redundant BDL nullability spec --- .../Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs index 01408ca087..cf97743fde 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs @@ -17,7 +17,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings { protected override LocalisableString Header => DebugSettingsStrings.GeneralHeader; - [BackgroundDependencyLoader(true)] + [BackgroundDependencyLoader] private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig, IPerformFromScreenRunner? performer) { Children = new Drawable[] From 1f2f522a1ee3e16a0df4d41a6f27fac7c169597b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 24 Jun 2023 15:07:04 +0200 Subject: [PATCH 0341/2100] Mark override as null-accepting `ModelBackedDrawable.CreateDrawable()` is R#-annotated to accept a potentially null model. Apply nullability there too for better reading experience. --- .../Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs index 67eedf655e..0bb60847e5 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs @@ -39,7 +39,7 @@ namespace osu.Game.Beatmaps.Drawables protected override double TransformDuration => 400; - protected override Drawable CreateDrawable(IBeatmapInfo model) + protected override Drawable CreateDrawable(IBeatmapInfo? model) { var drawable = getDrawableForModel(model); drawable.RelativeSizeAxes = Axes.Both; From 8fdd599b39cb50005177312b73cadfa62405a810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 24 Jun 2023 15:17:14 +0200 Subject: [PATCH 0342/2100] Match field NRT annotation in ctor argument --- osu.Game/Overlays/Changelog/ChangelogListing.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Changelog/ChangelogListing.cs b/osu.Game/Overlays/Changelog/ChangelogListing.cs index c4320dcbd0..5f1ae5b6fa 100644 --- a/osu.Game/Overlays/Changelog/ChangelogListing.cs +++ b/osu.Game/Overlays/Changelog/ChangelogListing.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Changelog { private readonly List? entries; - public ChangelogListing(List entries) + public ChangelogListing(List? entries) { this.entries = entries; } From 66ef199fa47a3b68047e8879284f44aca2de6007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 24 Jun 2023 15:35:07 +0200 Subject: [PATCH 0343/2100] Revert nullability enable in `Score` (and related changes) Causes several knock-on inspections in `OsuGame` et al. Probably best addressed in a separate pass, because treatment is mixed at best (some places nullcheck, some expect non-null). --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 15 +++++++++------ osu.Game/Scoring/Score.cs | 2 ++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 2a54ef16c6..f71da6c7e0 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -128,14 +128,17 @@ namespace osu.Game.Scoring.Legacy int lastTime = 0; - foreach (var f in score.Replay.Frames) + if (score.Replay != null) { - var legacyFrame = getLegacyFrame(f); + foreach (var f in score.Replay.Frames) + { + var legacyFrame = getLegacyFrame(f); - // Rounding because stable could only parse integral values - int time = (int)Math.Round(legacyFrame.Time + offset); - replayData.Append(FormattableString.Invariant($"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState},")); - lastTime = time; + // Rounding because stable could only parse integral values + int time = (int)Math.Round(legacyFrame.Time + offset); + replayData.Append(FormattableString.Invariant($"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState},")); + lastTime = time; + } } // Warning: this is purposefully hardcoded as a string rather than interpolating, as in some cultures the minus sign is not encoded as the standard ASCII U+00C2 codepoint, diff --git a/osu.Game/Scoring/Score.cs b/osu.Game/Scoring/Score.cs index 3323706ac1..7152f93f94 100644 --- a/osu.Game/Scoring/Score.cs +++ b/osu.Game/Scoring/Score.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable disable + using osu.Game.Replays; using osu.Game.Utils; From 2c1a44da895b0be4140704c0fcdca0a614efb3ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 24 Jun 2023 15:40:06 +0200 Subject: [PATCH 0344/2100] Revert nullability enable in `BeatmapBackground` Due to varying expectations in handling of `Beatmap`. Some places allow or expect null and some don't. Needs to be looked at closer separately. --- osu.Game/Graphics/Backgrounds/BeatmapBackground.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs index b8de0f7f1e..685f03ae56 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable disable + using osu.Framework.Allocation; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; @@ -22,7 +24,7 @@ namespace osu.Game.Graphics.Backgrounds [BackgroundDependencyLoader] private void load(LargeTextureStore textures) { - Sprite.Texture = Beatmap.GetBackground() ?? textures.Get(fallbackTextureName); + Sprite.Texture = Beatmap?.GetBackground() ?? textures.Get(fallbackTextureName); } public override bool Equals(Background other) From caf5673b68f3157d09964f487d94aa467cab9ca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 24 Jun 2023 16:05:19 +0200 Subject: [PATCH 0345/2100] Revert nullability enables in tournament client Not trivial to fix right now and I'm not fixing in a 1k-line changeset. --- osu.Game.Tournament/Components/DateTextBox.cs | 2 ++ osu.Game.Tournament/Models/LadderInfo.cs | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs index 4fa94b6c63..f23ad20a67 100644 --- a/osu.Game.Tournament/Components/DateTextBox.cs +++ b/osu.Game.Tournament/Components/DateTextBox.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable disable + using System; using osu.Framework.Bindables; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs index 3defd517cd..229837c94e 100644 --- a/osu.Game.Tournament/Models/LadderInfo.cs +++ b/osu.Game.Tournament/Models/LadderInfo.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable disable + using System; using System.Collections.Generic; using Newtonsoft.Json; @@ -25,7 +27,7 @@ namespace osu.Game.Tournament.Models public List Progressions = new List(); [JsonIgnore] // updated manually in TournamentGameBase - public Bindable CurrentMatch = new Bindable(); + public Bindable CurrentMatch = new Bindable(); public Bindable ChromaKeyWidth = new BindableInt(1024) { From e3a89a6273b01487b5c74130ab66eeedbd1ffaf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 24 Jun 2023 16:07:01 +0200 Subject: [PATCH 0346/2100] Fix remaining obvious CI inspections --- .../Edit/DrawableManiaEditorRuleset.cs | 2 +- .../Difficulty/Evaluators/SpeedEvaluator.cs | 2 +- .../Visual/Online/TestSceneNowPlayingCommand.cs | 5 +++-- osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs | 10 ++++++---- .../Ranking/TestSceneExpandedPanelMiddleContent.cs | 2 +- osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- .../Containers/Markdown/OsuMarkdownContainer.cs | 7 +++++++ osu.Game/Online/API/APIException.cs | 2 +- osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs | 4 ++-- osu.Game/Overlays/BeatmapSet/Info.cs | 2 +- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 2 +- 12 files changed, 26 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs index 1e9085bb2f..f480fa516b 100644 --- a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs +++ b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Edit { public new IScrollingInfo ScrollingInfo => base.ScrollingInfo; - public DrawableManiaEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) + public DrawableManiaEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods) : base(ruleset, beatmap, mods) { } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index 1ae500ec78..2df383aaa8 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // derive strainTime for calculation var osuCurrObj = (OsuDifficultyHitObject)current; var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null; - var osuNextObj = (OsuDifficultyHitObject)current.Next(0); + var osuNextObj = (OsuDifficultyHitObject?)current.Next(0); double strainTime = osuCurrObj.StrainTime; double doubletapness = 1; diff --git a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs index 10c2b2b9e1..fb36580a42 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs @@ -10,6 +10,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Chat; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Users; @@ -32,7 +33,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestGenericActivity() { - AddStep("Set activity", () => api.Activity.Value = new UserActivity.InLobby(null)); + AddStep("Set activity", () => api.Activity.Value = new UserActivity.InLobby(new Room())); AddStep("Run command", () => Add(new NowPlayingCommand(new Channel()))); @@ -63,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online [TestCase(false)] public void TestLinkPresence(bool hasOnlineId) { - AddStep("Set activity", () => api.Activity.Value = new UserActivity.InLobby(null)); + AddStep("Set activity", () => api.Activity.Value = new UserActivity.InLobby(new Room())); AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null) { diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index a047e2f0c5..c61b572d8c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -9,8 +9,10 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Tests.Beatmaps; using osu.Game.Users; @@ -116,9 +118,9 @@ namespace osu.Game.Tests.Visual.Online AddStep("solo (osu!catch)", () => activity.Value = soloGameStatusForRuleset(2)); AddStep("solo (osu!mania)", () => activity.Value = soloGameStatusForRuleset(3)); AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap()); - AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(null)); - AddStep("modding beatmap", () => activity.Value = new UserActivity.ModdingBeatmap(null)); - AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(null, null)); + AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo())); + AddStep("modding beatmap", () => activity.Value = new UserActivity.ModdingBeatmap(new BeatmapInfo())); + AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(new BeatmapInfo(), new OsuRuleset().RulesetInfo)); } [Test] @@ -134,7 +136,7 @@ namespace osu.Game.Tests.Visual.Online AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent); } - private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(null, rulesetStore.GetRuleset(rulesetId)); + private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!); private ScoreInfo createScore(string name) => new ScoreInfo(new TestBeatmap(Ruleset.Value).BeatmapInfo) { diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index bd7a11b4bb..c05774400f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -93,7 +93,7 @@ namespace osu.Game.Tests.Visual.Ranking private BeatmapInfo createTestBeatmap([NotNull] RealmUser author) { - var beatmap = new TestBeatmap(rulesetStore.GetRuleset(0)).BeatmapInfo; + var beatmap = new TestBeatmap(rulesetStore.GetRuleset(0)!).BeatmapInfo; beatmap.Metadata.Author = author; beatmap.Metadata.Title = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap title"; diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 8d5547c749..6b35102014 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -177,7 +177,7 @@ namespace osu.Game.Tournament.Screens.Schedule Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - new TournamentSpriteTextWithBackground(match.NewValue.Round.Value?.Name.Value) + new TournamentSpriteTextWithBackground(match.NewValue.Round.Value?.Name.Value ?? string.Empty) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index a5fc815a5e..041b00c7e1 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -93,7 +93,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}")); writer.WriteLine(FormattableString.Invariant($"Countdown: {(int)beatmap.BeatmapInfo.Countdown}")); writer.WriteLine(FormattableString.Invariant( - $"SampleSet: {toLegacySampleBank(((beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePoints?.FirstOrDefault() ?? SampleControlPoint.DEFAULT).SampleBank)}")); + $"SampleSet: {toLegacySampleBank(((beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePoints.FirstOrDefault() ?? SampleControlPoint.DEFAULT).SampleBank)}")); writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.BeatmapInfo.StackLeniency}")); writer.WriteLine(FormattableString.Invariant($"Mode: {onlineRulesetID}")); writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? '1' : '0')}")); diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs index 4a61ee2043..b478c4757f 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs @@ -35,6 +35,13 @@ namespace osu.Game.Graphics.Containers.Markdown break; case ListItemBlock listItemBlock: + // `ListBlock.Parent` is annotated as null-returning in xmldoc. + // Unfortunately code analysis sees that the type doesn't have NRT enabled and complains. + // This is fixed upstream in 0.24.0 (https://github.com/xoofx/markdig/commit/6684c8257cbbcba2d34457020876be289d3cd8b9), + // but markdig is a transitive dependency from framework, wherein we are locked to 0.23.0 + // (https://github.com/ppy/osu-framework/blob/9746d7d06f48910c05a24687a25f435f30d12f8b/osu.Framework/osu.Framework.csproj#L52C1-L54) + // Therefore... + // ReSharper disable once ConstantConditionalAccessQualifier bool isOrdered = ((ListBlock)listItemBlock.Parent)?.IsOrdered == true; OsuMarkdownListItem childContainer = CreateListItem(listItemBlock, level, isOrdered); diff --git a/osu.Game/Online/API/APIException.cs b/osu.Game/Online/API/APIException.cs index 7491e375df..4327600e13 100644 --- a/osu.Game/Online/API/APIException.cs +++ b/osu.Game/Online/API/APIException.cs @@ -7,7 +7,7 @@ namespace osu.Game.Online.API { public class APIException : InvalidOperationException { - public APIException(string message, Exception innerException) + public APIException(string message, Exception? innerException) : base(message, innerException) { } diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs b/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs index 7d8160bef7..426fbcdb8d 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs @@ -11,9 +11,9 @@ namespace osu.Game.Overlays.BeatmapSet { public partial class BeatmapRulesetSelector : OverlayRulesetSelector { - private readonly Bindable beatmapSet = new Bindable(); + private readonly Bindable beatmapSet = new Bindable(); - public APIBeatmapSet BeatmapSet + public APIBeatmapSet? BeatmapSet { get => beatmapSet.Value; set diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs index c182ef2e15..d21b2546b9 100644 --- a/osu.Game/Overlays/BeatmapSet/Info.cs +++ b/osu.Game/Overlays/BeatmapSet/Info.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.BeatmapSet public readonly Bindable BeatmapSet = new Bindable(); - public APIBeatmap BeatmapInfo + public APIBeatmap? BeatmapInfo { get => successRate.Beatmap; set => successRate.Beatmap = value; diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 961f8684ce..5dd2486e1c 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -351,7 +351,7 @@ namespace osu.Game.Screens.Select private void addInfoLabels() { - if (working.Beatmap?.HitObjects?.Any() != true) + if (working.Beatmap?.HitObjects.Any() != true) return; infoLabelContainer.Children = new Drawable[] From 34e25403313a0a5926403aac69cd518c5d483c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 24 Jun 2023 17:05:52 +0200 Subject: [PATCH 0347/2100] Fix nullability-related warnings in Android project --- osu.Android/GameplayScreenRotationLocker.cs | 2 +- osu.Android/OsuGameActivity.cs | 15 ++++++++------- osu.Android/OsuGameAndroid.cs | 7 ++++--- osu.Game.Tests.Android/MainActivity.cs | 2 +- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs index d77b24722a..e5fc354db7 100644 --- a/osu.Android/GameplayScreenRotationLocker.cs +++ b/osu.Android/GameplayScreenRotationLocker.cs @@ -11,7 +11,7 @@ namespace osu.Android { public partial class GameplayScreenRotationLocker : Component { - private Bindable localUserPlaying; + private Bindable localUserPlaying = null!; [Resolved] private OsuGameActivity gameActivity { get; set; } = null!; diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 81b218436e..33ffed432e 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -13,6 +13,7 @@ using Android.Graphics; using Android.OS; using Android.Views; using osu.Framework.Android; +using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Database; using Debug = System.Diagnostics.Debug; using Uri = Android.Net.Uri; @@ -49,11 +50,11 @@ namespace osu.Android /// Adjusted on startup to match expected UX for the current device type (phone/tablet). public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified; - private OsuGameAndroid game; + private OsuGameAndroid game = null!; protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this); - protected override void OnCreate(Bundle savedInstanceState) + protected override void OnCreate(Bundle? savedInstanceState) { base.OnCreate(savedInstanceState); @@ -90,15 +91,15 @@ namespace osu.Android Assembly.Load("osu.Game.Rulesets.Mania"); } - protected override void OnNewIntent(Intent intent) => handleIntent(intent); + protected override void OnNewIntent(Intent? intent) => handleIntent(intent); - private void handleIntent(Intent intent) + private void handleIntent(Intent? intent) { - switch (intent.Action) + switch (intent?.Action) { case Intent.ActionDefault: if (intent.Scheme == ContentResolver.SchemeContent) - handleImportFromUris(intent.Data); + handleImportFromUris(intent.Data.AsNonNull()); else if (osu_url_schemes.Contains(intent.Scheme)) game.HandleLink(intent.DataString); break; @@ -112,7 +113,7 @@ namespace osu.Android { var content = intent.ClipData?.GetItemAt(i); if (content != null) - uris.Add(content.Uri); + uris.Add(content.Uri.AsNonNull()); } handleImportFromUris(uris.ToArray()); diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 96f81c209c..dea70e6b27 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -6,6 +6,7 @@ using Android.App; using Microsoft.Maui.Devices; using osu.Framework.Allocation; using osu.Framework.Android.Input; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Input.Handlers; using osu.Framework.Platform; using osu.Game; @@ -30,7 +31,7 @@ namespace osu.Android { get { - var packageInfo = Application.Context.ApplicationContext.PackageManager.GetPackageInfo(Application.Context.ApplicationContext.PackageName, 0); + var packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull(); try { @@ -43,7 +44,7 @@ namespace osu.Android // Basic conversion format (as done in Fastfile): 2020.606.0 -> 202006060 // https://stackoverflow.com/questions/52977079/android-sdk-28-versioncode-in-packageinfo-has-been-deprecated - string versionName = string.Empty; + string versionName; if (OperatingSystem.IsAndroidVersionAtLeast(28)) { @@ -66,7 +67,7 @@ namespace osu.Android { } - return new Version(packageInfo.VersionName); + return new Version(packageInfo.VersionName.AsNonNull()); } } diff --git a/osu.Game.Tests.Android/MainActivity.cs b/osu.Game.Tests.Android/MainActivity.cs index ab43a6766c..d25e46f3c5 100644 --- a/osu.Game.Tests.Android/MainActivity.cs +++ b/osu.Game.Tests.Android/MainActivity.cs @@ -13,7 +13,7 @@ namespace osu.Game.Tests.Android { protected override Framework.Game CreateGame() => new OsuTestBrowser(); - protected override void OnCreate(Bundle savedInstanceState) + protected override void OnCreate(Bundle? savedInstanceState) { base.OnCreate(savedInstanceState); From df2dcf85b4b4e7e54755d6d9e56b43ae04e787c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 24 Jun 2023 17:07:42 +0200 Subject: [PATCH 0348/2100] Fix wrong disable --- osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs index b478c4757f..5da785603a 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Graphics.Containers.Markdown // but markdig is a transitive dependency from framework, wherein we are locked to 0.23.0 // (https://github.com/ppy/osu-framework/blob/9746d7d06f48910c05a24687a25f435f30d12f8b/osu.Framework/osu.Framework.csproj#L52C1-L54) // Therefore... - // ReSharper disable once ConstantConditionalAccessQualifier + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract bool isOrdered = ((ListBlock)listItemBlock.Parent)?.IsOrdered == true; OsuMarkdownListItem childContainer = CreateListItem(listItemBlock, level, isOrdered); From e273c223a8afd7a6c22e3e7de300117fe8832762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 24 Jun 2023 17:11:38 +0200 Subject: [PATCH 0349/2100] Fix some more missed CI inspections --- .../Visual/Online/TestSceneBeatmapRulesetSelector.cs | 2 +- osu.Game/Online/Rooms/JoinRoomRequest.cs | 4 ++-- .../Rulesets/Difficulty/PerformanceBreakdownCalculator.cs | 3 ++- osu.Game/Screens/OnlinePlay/Match/RoomBackgroundScreen.cs | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs index 36c3576da6..599eee7d0c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Online selector.BeatmapSet = new APIBeatmapSet { - Beatmaps = selector.BeatmapSet.Beatmaps + Beatmaps = selector.BeatmapSet!.Beatmaps .Where(b => b.Ruleset.OnlineID != ruleset) .Concat(Enumerable.Range(0, count).Select(_ => new APIBeatmap { RulesetID = ruleset })) .ToArray(), diff --git a/osu.Game/Online/Rooms/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs index a1d6ed1e82..8645f2a2c0 100644 --- a/osu.Game/Online/Rooms/JoinRoomRequest.cs +++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs @@ -10,9 +10,9 @@ namespace osu.Game.Online.Rooms public class JoinRoomRequest : APIRequest { public readonly Room Room; - public readonly string Password; + public readonly string? Password; - public JoinRoomRequest(Room room, string password) + public JoinRoomRequest(Room room, string? password) { Room = room; Password = password; diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 64a04f896f..8b59500f43 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; +using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -88,7 +89,7 @@ namespace osu.Game.Rulesets.Difficulty ).ConfigureAwait(false); // ScorePerformanceCache is not used to avoid caching multiple copies of essentially identical perfect performance attributes - return difficulty == null ? null : ruleset.CreatePerformanceCalculator()?.Calculate(perfectPlay, difficulty.Value.Attributes); + return difficulty == null ? null : ruleset.CreatePerformanceCalculator()?.Calculate(perfectPlay, difficulty.Value.Attributes.AsNonNull()); }, cancellationToken); } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomBackgroundScreen.cs index 982275f96a..53a52a8cb8 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomBackgroundScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomBackgroundScreen.cs @@ -9,9 +9,9 @@ namespace osu.Game.Screens.OnlinePlay.Match { public partial class RoomBackgroundScreen : OnlinePlayBackgroundScreen { - public readonly Bindable SelectedItem = new Bindable(); + public readonly Bindable SelectedItem = new Bindable(); - public RoomBackgroundScreen(PlaylistItem initialPlaylistItem) + public RoomBackgroundScreen(PlaylistItem? initialPlaylistItem) { PlaylistItem = initialPlaylistItem; SelectedItem.BindValueChanged(item => PlaylistItem = item.NewValue); From 9a5f033a0fa6a066d8994322777f2478780463e9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 24 Jun 2023 18:11:34 +0300 Subject: [PATCH 0350/2100] Change `OpenUserProfile` argument type to always use `IUser` --- osu.Game/Graphics/Containers/LinkFlowContainer.cs | 8 +------- osu.Game/Online/Chat/MessageFormatter.cs | 14 +++++++++++--- osu.Game/OsuGame.cs | 7 +------ .../Sections/Recent/DrawableRecentActivity.cs | 2 +- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index a2c9b0ac32..2d27ce906b 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -74,13 +74,7 @@ namespace osu.Game.Graphics.Containers } public void AddUserLink(IUser user, Action creationParameters = null) - { - string argument = user.OnlineID > 1 - ? user.OnlineID.ToString() - : user.Username; - - createLink(CreateChunkFor(user.Username, true, CreateSpriteText, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, argument), "view profile"); - } + => createLink(CreateChunkFor(user.Username, true, CreateSpriteText, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user), "view profile"); private void createLink(ITextPart textPart, LinkDetails link, LocalisableString tooltipText, Action action = null) { diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 523185a7cb..f89939d7cf 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.Chat { @@ -172,7 +173,7 @@ namespace osu.Game.Online.Chat case "u": case "users": - return new LinkDetails(LinkAction.OpenUserProfile, mainArg); + return getUserLink(mainArg); case "wiki": return new LinkDetails(LinkAction.OpenWiki, string.Join('/', args.Skip(3))); @@ -230,8 +231,7 @@ namespace osu.Game.Online.Chat break; case "u": - linkType = LinkAction.OpenUserProfile; - break; + return getUserLink(args[2]); default: return new LinkDetails(LinkAction.External, url); @@ -246,6 +246,14 @@ namespace osu.Game.Online.Chat return new LinkDetails(LinkAction.External, url); } + private static LinkDetails getUserLink(string argument) + { + if (int.TryParse(argument, out int userId)) + return new LinkDetails(LinkAction.OpenUserProfile, new APIUser { Id = userId }); + + return new LinkDetails(LinkAction.OpenUserProfile, new APIUser { Username = argument }); + } + private static MessageFormatterResult format(string toFormat, int startIndex = 0, int space = 3) { var result = new MessageFormatterResult(toFormat); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 2db8872524..d8eb63caee 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -45,7 +45,6 @@ using osu.Game.Input.Bindings; using osu.Game.IO; using osu.Game.Localisation; using osu.Game.Online; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; @@ -446,11 +445,7 @@ namespace osu.Game break; case LinkAction.OpenUserProfile: - var user = int.TryParse(argString, out int userId) - ? new APIUser { Id = userId } - : new APIUser { Username = argString }; - - ShowUser(user); + ShowUser((IUser)link.Argument); break; case LinkAction.OpenWiki: diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs index 0479ab7c16..8a0003b4ea 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs @@ -223,7 +223,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent private void addBeatmapsetLink() => content.AddLink(activity.Beatmapset.AsNonNull().Title, LinkAction.OpenBeatmapSet, getLinkArgument(activity.Beatmapset.AsNonNull().Url), creationParameters: t => t.Font = getLinkFont()); - private string getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.WebsiteRootUrl}{url}").Argument.ToString().AsNonNull(); + private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.WebsiteRootUrl}{url}").Argument.AsNonNull(); private FontUsage getLinkFont(FontWeight fontWeight = FontWeight.Regular) => OsuFont.GetFont(size: font_size, weight: fontWeight, italics: true); From ca402c4d2f4470cba5cd4f21d4a8a4eb15bf5047 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 25 Jun 2023 00:38:19 +0900 Subject: [PATCH 0351/2100] Update framework --- 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 66f518f3d5..39c10c2014 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -11,7 +11,7 @@ manifestmerger.jar - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 9cb20ee364..d179d354fb 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 256d1e43c4..85cbe3f14b 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -16,6 +16,6 @@ iossimulator-x64 - + From f8d2f2f7e1f9f364700b7b10169b667ca0c32837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 24 Jun 2023 19:04:05 +0200 Subject: [PATCH 0352/2100] Fix more issues discovered by CI that can be fixed game-side --- .../Editor/TestScenePathControlPointVisualiser.cs | 2 +- .../Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 37561fda85..0d6841017e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { MenuItem item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == "Curve type")?.Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText); - item?.Action?.Value(); + item?.Action.Value?.Invoke(); }); } } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs index 224e7e411e..5467a64b85 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs @@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.Navigation private OsuButton configureBindingsButton => Game.Settings .ChildrenOfType().SingleOrDefault()? - .ChildrenOfType()? + .ChildrenOfType() .First(b => b.Text.ToString() == "Configure"); private KeyBindingPanel keyBindingPanel => Game.Settings From 5806153cfd6dd176cb584ea700a8416f0bb52a5f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 25 Jun 2023 12:00:14 +0900 Subject: [PATCH 0353/2100] Update framework --- 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 39c10c2014..fdec4e575b 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -11,7 +11,7 @@ manifestmerger.jar - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index d179d354fb..ce03dca949 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 85cbe3f14b..86694e268a 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -16,6 +16,6 @@ iossimulator-x64 - + From 922fe927ac62ddd2af867093d7c36d26fe043f7f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 25 Jun 2023 15:18:56 +0900 Subject: [PATCH 0354/2100] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ce03dca949..b4d8dd513f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From 0dced4610035b07c48856324314c3be5a18af92f Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Sun, 25 Jun 2023 20:46:28 +0900 Subject: [PATCH 0355/2100] remove `#nullable disable` in `LadderInfo` --- osu.Game.Tournament/Models/LadderInfo.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs index 229837c94e..3defd517cd 100644 --- a/osu.Game.Tournament/Models/LadderInfo.cs +++ b/osu.Game.Tournament/Models/LadderInfo.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using Newtonsoft.Json; @@ -27,7 +25,7 @@ namespace osu.Game.Tournament.Models public List Progressions = new List(); [JsonIgnore] // updated manually in TournamentGameBase - public Bindable CurrentMatch = new Bindable(); + public Bindable CurrentMatch = new Bindable(); public Bindable ChromaKeyWidth = new BindableInt(1024) { From a3cd0d14a3f3caea616a3ab635a9f5f67463787e Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Sun, 25 Jun 2023 20:46:58 +0900 Subject: [PATCH 0356/2100] null guard for Current Match in MatchMessage --- .../TestSceneTournamentMatchChatDisplay.cs | 24 +++++++++---------- .../Components/TournamentMatchChatDisplay.cs | 10 ++++---- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs index f627bd2ad2..bf59678ac5 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs @@ -63,18 +63,6 @@ namespace osu.Game.Tournament.Tests.Components Origin = Anchor.Centre, }); - ladderInfo.CurrentMatch.Value = new TournamentMatch - { - Team1 = - { - Value = new TournamentTeam { Players = new BindableList { redUser } } - }, - Team2 = - { - Value = new TournamentTeam { Players = new BindableList { blueUser, blueUserWithCustomColour } } - } - }; - chatDisplay.Channel.Value = testChannel; } @@ -88,6 +76,18 @@ namespace osu.Game.Tournament.Tests.Components Content = "I am a wang!" })); + AddStep("set Current match", () => ladderInfo.CurrentMatch.Value = new TournamentMatch + { + Team1 = + { + Value = new TournamentTeam { Players = new BindableList { redUser } } + }, + Team2 = + { + Value = new TournamentTeam { Players = new BindableList { blueUser, blueUserWithCustomColour } } + } + }); + AddStep("message from team red", () => testChannel.AddNewMessages(new Message(nextMessageId()) { Sender = redUser.ToAPIUser(), diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index 2a2e45d70c..bb8d86e7e2 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -19,10 +17,10 @@ namespace osu.Game.Tournament.Components { private readonly Bindable chatChannel = new Bindable(); - private ChannelManager manager; + private ChannelManager? manager; [Resolved] - private LadderInfo ladderInfo { get; set; } + private LadderInfo ladderInfo { get; set; } = null!; public TournamentMatchChatDisplay() { @@ -35,7 +33,7 @@ namespace osu.Game.Tournament.Components } [BackgroundDependencyLoader(true)] - private void load(MatchIPCInfo ipc, IAPIProvider api) + private void load(MatchIPCInfo? ipc, IAPIProvider api) { if (ipc != null) { @@ -92,6 +90,8 @@ namespace osu.Game.Tournament.Components public MatchMessage(Message message, LadderInfo info) : base(message) { + if (info.CurrentMatch.Value == null) return; + if (info.CurrentMatch.Value.Team1.Value.Players.Any(u => u.OnlineID == Message.Sender.OnlineID)) UsernameColour = TournamentGame.COLOUR_RED; else if (info.CurrentMatch.Value.Team2.Value.Players.Any(u => u.OnlineID == Message.Sender.OnlineID)) From 95e8dd2e8ef27669bf9a19a326fde1d6397c35f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 25 Jun 2023 21:25:07 +0900 Subject: [PATCH 0357/2100] Don't attempt to access notifications before loaded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Overlays/NotificationOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 28803fe590..f1e39b947d 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -35,7 +35,7 @@ namespace osu.Game.Overlays public const float TRANSITION_LENGTH = 600; public IEnumerable AllNotifications => - toastTray.Notifications.Concat(sections.SelectMany(s => s.Notifications)); + IsLoaded ? toastTray.Notifications.Concat(sections.SelectMany(s => s.Notifications)) : Array.Empty(); private FlowContainer sections = null!; From 3d1a8aeb540799e3d62fc915805b3dd3678beb69 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 25 Jun 2023 21:24:39 +0900 Subject: [PATCH 0358/2100] Use more understandable cancel button text --- osu.Game/Screens/Menu/ConfirmExitDialog.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/ConfirmExitDialog.cs b/osu.Game/Screens/Menu/ConfirmExitDialog.cs index fb22f7eff8..0041d047bd 100644 --- a/osu.Game/Screens/Menu/ConfirmExitDialog.cs +++ b/osu.Game/Screens/Menu/ConfirmExitDialog.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; +using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; @@ -52,7 +53,7 @@ namespace osu.Game.Screens.Menu }, new PopupDialogCancelButton { - Text = @"Cancel", + Text = CommonStrings.Back, Action = onCancel }, }; From cf43cd2bdcc165eca96d7b569b2f26a617e049bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 25 Jun 2023 21:26:08 +0900 Subject: [PATCH 0359/2100] Rename test scene to match updated class name --- ...dToConfirmOverlay.cs => TestSceneHoldToExitGameOverlay.cs} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename osu.Game.Tests/Visual/UserInterface/{TestSceneHoldToConfirmOverlay.cs => TestSceneHoldToExitGameOverlay.cs} (94%) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToExitGameOverlay.cs similarity index 94% rename from osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneHoldToExitGameOverlay.cs index 58d1637ca7..df423268b6 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToExitGameOverlay.cs @@ -8,11 +8,11 @@ using osu.Game.Screens.Menu; namespace osu.Game.Tests.Visual.UserInterface { - public partial class TestSceneHoldToConfirmOverlay : OsuTestScene + public partial class TestSceneHoldToExitGameOverlay : OsuTestScene { protected override double TimePerAction => 100; // required for the early exit test, since hold-to-confirm delay is 200ms - public TestSceneHoldToConfirmOverlay() + public TestSceneHoldToExitGameOverlay() { bool fired = false; From 4215ca313f17f147d3b0ca2d2feaeb04abf062bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 25 Jun 2023 14:36:21 +0200 Subject: [PATCH 0360/2100] Add missing using --- osu.Game/Overlays/NotificationOverlay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index f1e39b947d..21027b0931 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.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.Linq; using osu.Framework.Allocation; From a7088ffe22f6b03475303dcda5425fde0aa9cee1 Mon Sep 17 00:00:00 2001 From: tsrk Date: Sun, 25 Jun 2023 15:04:39 +0200 Subject: [PATCH 0361/2100] revert: bring back old attachment flow As discussed, this would bring more problems that anything. Refs: 4c39708, f83a4f4 --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 11 +++- osu.Game/Rulesets/UI/IKeybindingListener.cs | 50 --------------- osu.Game/Rulesets/UI/RulesetInputManager.cs | 63 ++++++++++--------- .../ClicksPerSecondCalculator.cs | 19 +----- .../Screens/Play/HUD/KeyCounterController.cs | 21 +------ osu.Game/Screens/Play/HUDOverlay.cs | 12 ++-- 6 files changed, 49 insertions(+), 127 deletions(-) delete mode 100644 osu.Game/Rulesets/UI/IKeybindingListener.cs diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index e0a1533c4b..57f5f54d8f 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -30,6 +30,8 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osuTK; namespace osu.Game.Rulesets.UI @@ -38,7 +40,7 @@ namespace osu.Game.Rulesets.UI /// Displays an interactive ruleset gameplay instance. /// /// The type of HitObject contained by this DrawableRuleset. - public abstract partial class DrawableRuleset : DrawableRuleset, IProvideCursor, IKeybindingEventsEmitter + public abstract partial class DrawableRuleset : DrawableRuleset, IProvideCursor, ICanAttachHUDPieces where TObject : HitObject { public override event Action NewResult; @@ -327,8 +329,11 @@ namespace osu.Game.Rulesets.UI /// The representing . public abstract DrawableHitObject CreateDrawableRepresentation(TObject h); - public void Attach(IKeybindingListener skinComponent) => - (KeyBindingInputManager as IKeybindingEventsEmitter)?.Attach(skinComponent); + public void Attach(KeyCounterController keyCounter) => + (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(keyCounter); + + public void Attach(ClicksPerSecondCalculator calculator) => + (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(calculator); /// /// Creates a key conversion input manager. An exception will be thrown if a valid is not returned. diff --git a/osu.Game/Rulesets/UI/IKeybindingListener.cs b/osu.Game/Rulesets/UI/IKeybindingListener.cs deleted file mode 100644 index f38ce8643e..0000000000 --- a/osu.Game/Rulesets/UI/IKeybindingListener.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable -using System.Collections.Generic; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; - -namespace osu.Game.Rulesets.UI -{ - /// - /// Listens to events emitted by an . - /// Alternative to for classes that need to not depend on type parameters. - /// - public interface IKeybindingListener - { - /// - /// This class or a member of this class can already handle keybindings. - /// Signals to the that and - /// don't necessarily need to be called. - /// - /// - /// This is usually true for s and s that need to - /// pass s events to children that can already handle them. - /// - public bool CanHandleKeybindings { get; } - - /// - /// Prepares this class to receive events. - /// - /// The list of possible actions that can occur. - /// The type actions, commonly enums. - public void Setup(IEnumerable actions) where T : struct; - - /// - /// Called when an action is pressed. - /// - /// The event containing information about the pressed action. - /// The type of binding, commonly enums. - public void OnPressed(KeyBindingPressEvent action) where T : struct; - - /// - /// Called when an action is released. - /// - /// The event containing information about the released action. - /// The type of binding, commonly enums. - public void OnReleased(KeyBindingReleaseEvent action) where T : struct; - } -} diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 44c1f00cf7..2c403139b6 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -19,11 +19,13 @@ using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Input.Handlers; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Play.HUD.ClicksPerSecond; using static osu.Game.Input.Handlers.ReplayInputHandler; namespace osu.Game.Rulesets.UI { - public abstract partial class RulesetInputManager : PassThroughInputManager, IKeybindingEventsEmitter, IHasReplayHandler, IHasRecordingHandler + public abstract partial class RulesetInputManager : PassThroughInputManager, ICanAttachHUDPieces, IHasReplayHandler, IHasRecordingHandler where T : struct { protected override bool AllowRightClickFromLongTouch => false; @@ -64,7 +66,6 @@ namespace osu.Game.Rulesets.UI InternalChild = KeyBindingContainer = CreateKeyBindingContainer(ruleset, variant, unique) .WithChild(content = new Container { RelativeSizeAxes = Axes.Both }); - KeyBindingContainer.Add(actionListener = new ActionListener()); } [BackgroundDependencyLoader(true)] @@ -157,49 +158,47 @@ namespace osu.Game.Rulesets.UI #endregion - #region Component attachement + #region Key Counter Attachment - private readonly ActionListener actionListener; - - public void Attach(IKeybindingListener skinComponent) + public void Attach(KeyCounterController keyCounter) { - skinComponent.Setup(KeyBindingContainer.DefaultKeyBindings + KeyBindingContainer.Add(keyCounter); + + keyCounter.AddRange(KeyBindingContainer.DefaultKeyBindings .Select(b => b.GetAction()) .Distinct() - .OrderBy(a => a)); + .OrderBy(action => action) + .Select(action => new KeyCounterActionTrigger(action))); + } - if (skinComponent.CanHandleKeybindings && skinComponent is Drawable component) - { - try - { - KeyBindingContainer.Add(component); - return; - } - catch (Exception) - { - return; - } - } + #endregion - actionListener.OnPressedEvent += skinComponent.OnPressed; - actionListener.OnReleasedEvent += skinComponent.OnReleased; + #region Keys per second Counter Attachment + + public void Attach(ClicksPerSecondCalculator calculator) + { + var listener = new ActionListener(calculator); + + KeyBindingContainer.Add(listener); } private partial class ActionListener : Component, IKeyBindingHandler { - public event Action> OnPressedEvent; + private readonly ClicksPerSecondCalculator calculator; - public event Action> OnReleasedEvent; + public ActionListener(ClicksPerSecondCalculator calculator) + { + this.calculator = calculator; + } public bool OnPressed(KeyBindingPressEvent e) { - OnPressedEvent?.Invoke(e); + calculator.AddInputTimestamp(); return false; } public void OnReleased(KeyBindingReleaseEvent e) { - OnReleasedEvent?.Invoke(e); } } @@ -240,11 +239,17 @@ namespace osu.Game.Rulesets.UI } /// - /// Sends events to a + /// Supports attaching various HUD pieces. + /// Keys will be populated automatically and a receptor will be injected inside. /// - public interface IKeybindingEventsEmitter + public interface ICanAttachHUDPieces + { + void Attach(KeyCounterController keyCounter); + void Attach(ClicksPerSecondCalculator calculator); + } + + public interface IAttachableSkinComponent { - void Attach(IKeybindingListener component); } public class RulesetInputManagerInputState : InputState diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs index e0e93cb66e..3e55e11f1c 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs @@ -4,12 +4,11 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Input.Events; using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Play.HUD.ClicksPerSecond { - public partial class ClicksPerSecondCalculator : Component, IKeybindingListener + public partial class ClicksPerSecondCalculator : Component, IAttachableSkinComponent { private readonly List timestamps = new List(); @@ -54,21 +53,5 @@ namespace osu.Game.Screens.Play.HUD.ClicksPerSecond Value = count; } - - #region IKeybindingListener - - bool IKeybindingListener.CanHandleKeybindings => false; - - void IKeybindingListener.Setup(IEnumerable actions) - { - } - - void IKeybindingListener.OnPressed(KeyBindingPressEvent action) => AddInputTimestamp(); - - void IKeybindingListener.OnReleased(KeyBindingReleaseEvent action) - { - } - - #endregion } } diff --git a/osu.Game/Screens/Play/HUD/KeyCounterController.cs b/osu.Game/Screens/Play/HUD/KeyCounterController.cs index 2e678e55fc..0fa02afbb4 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterController.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterController.cs @@ -3,16 +3,14 @@ using System; using System.Collections.Generic; -using System.Linq; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Play.HUD { - public partial class KeyCounterController : CompositeComponent, IKeybindingListener + public partial class KeyCounterController : CompositeComponent, IAttachableSkinComponent { public readonly Bindable IsCounting = new BindableBool(true); @@ -38,22 +36,5 @@ namespace osu.Game.Screens.Play.HUD public override bool HandleNonPositionalInput => true; public override bool HandlePositionalInput => true; - - #region IKeybindingListener - - bool IKeybindingListener.CanHandleKeybindings => true; - - void IKeybindingListener.Setup(IEnumerable actions) - => AddRange(actions.Select(a => new KeyCounterActionTrigger(a))); - - void IKeybindingListener.OnPressed(KeyBindingPressEvent action) - { - } - - void IKeybindingListener.OnReleased(KeyBindingReleaseEvent action) - { - } - - #endregion } } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index b74b5d835a..21636ac04c 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -10,7 +10,6 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; @@ -103,8 +102,6 @@ namespace osu.Game.Screens.Play private readonly List hideTargets; - private readonly IEnumerable actionInjectionCandidates; - public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList mods, bool alwaysShowLeaderboard = true) { Drawable rulesetComponents; @@ -166,8 +163,6 @@ namespace osu.Game.Screens.Play hideTargets = new List { mainComponents, rulesetComponents, topRightElements }; - actionInjectionCandidates = new IKeybindingListener[] { clicksPerSecondCalculator, KeyCounter }; - if (!alwaysShowLeaderboard) hideTargets.Add(LeaderboardFlow); } @@ -324,8 +319,11 @@ namespace osu.Game.Screens.Play protected virtual void BindDrawableRuleset(DrawableRuleset drawableRuleset) { - if (drawableRuleset is IKeybindingEventsEmitter attachTarget) - actionInjectionCandidates.ForEach(attachTarget.Attach); + if (drawableRuleset is ICanAttachHUDPieces attachTarget) + { + attachTarget.Attach(KeyCounter); + attachTarget.Attach(clicksPerSecondCalculator); + } replayLoaded.BindTo(drawableRuleset.HasReplayLoaded); } From e02a06e3e557f793f4c61d688cf8712524f9bc08 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Sun, 25 Jun 2023 22:47:36 +0900 Subject: [PATCH 0362/2100] Revert "remove `#nullable disable` in `LadderInfo`" This reverts commit 0dced4610035b07c48856324314c3be5a18af92f. --- osu.Game.Tournament/Models/LadderInfo.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs index 3defd517cd..229837c94e 100644 --- a/osu.Game.Tournament/Models/LadderInfo.cs +++ b/osu.Game.Tournament/Models/LadderInfo.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable disable + using System; using System.Collections.Generic; using Newtonsoft.Json; @@ -25,7 +27,7 @@ namespace osu.Game.Tournament.Models public List Progressions = new List(); [JsonIgnore] // updated manually in TournamentGameBase - public Bindable CurrentMatch = new Bindable(); + public Bindable CurrentMatch = new Bindable(); public Bindable ChromaKeyWidth = new BindableInt(1024) { From 14e26d2a850e8e366c25ed0e39456024cca39e42 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Sun, 25 Jun 2023 22:42:02 +0900 Subject: [PATCH 0363/2100] use BeatmapCache for populate beatmap information --- osu.Game.Tournament/TournamentGameBase.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 634cc87a9f..2067295eb6 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -15,6 +15,7 @@ using osu.Framework.Input; using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Online; using osu.Game.Online.API.Requests; @@ -35,6 +36,7 @@ namespace osu.Game.Tournament private TournamentStorage storage; private DependencyContainer dependencies; private FileBasedIPC ipc; + private BeatmapLookupCache beatmapCache; protected Task BracketLoadTask => bracketLoadTaskCompletionSource.Task; @@ -75,6 +77,8 @@ namespace osu.Game.Tournament Textures.AddTextureSource(new TextureLoaderStore(new StorageBackedResourceStore(storage))); dependencies.CacheAs(new StableInfo(storage)); + + beatmapCache = dependencies.Get(); } protected override void LoadComplete() @@ -241,9 +245,7 @@ namespace osu.Game.Tournament { var b = beatmapsRequiringPopulation[i]; - var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = b.ID }); - API.Perform(req); - b.Beatmap = new TournamentBeatmap(req.Response ?? new APIBeatmap()); + b.Beatmap = new TournamentBeatmap(beatmapCache.GetBeatmapAsync(b.ID).GetAwaiter().GetResult() ?? new APIBeatmap()); updateLoadProgressMessage($"Populating round beatmaps ({i} / {beatmapsRequiringPopulation.Count})"); } @@ -268,9 +270,7 @@ namespace osu.Game.Tournament { var b = beatmapsRequiringPopulation[i]; - var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = b.ID }); - API.Perform(req); - b.Beatmap = new TournamentBeatmap(req.Response ?? new APIBeatmap()); + b.Beatmap = new TournamentBeatmap(beatmapCache.GetBeatmapAsync(b.ID).GetAwaiter().GetResult() ?? new APIBeatmap()); updateLoadProgressMessage($"Populating seeding beatmaps ({i} / {beatmapsRequiringPopulation.Count})"); } From 9862992af0b415b3bfb4685196d35347ddd9397b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 25 Jun 2023 17:36:44 +0200 Subject: [PATCH 0364/2100] Use alternative guard Early-returning from ctors feels pretty bad. Also saves on some nested accesses. --- .../Components/TournamentMatchChatDisplay.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index bb8d86e7e2..15c37474f5 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -90,12 +90,13 @@ namespace osu.Game.Tournament.Components public MatchMessage(Message message, LadderInfo info) : base(message) { - if (info.CurrentMatch.Value == null) return; - - if (info.CurrentMatch.Value.Team1.Value.Players.Any(u => u.OnlineID == Message.Sender.OnlineID)) - UsernameColour = TournamentGame.COLOUR_RED; - else if (info.CurrentMatch.Value.Team2.Value.Players.Any(u => u.OnlineID == Message.Sender.OnlineID)) - UsernameColour = TournamentGame.COLOUR_BLUE; + if (info.CurrentMatch.Value is TournamentMatch match) + { + if (match.Team1.Value.Players.Any(u => u.OnlineID == Message.Sender.OnlineID)) + UsernameColour = TournamentGame.COLOUR_RED; + else if (match.Team2.Value.Players.Any(u => u.OnlineID == Message.Sender.OnlineID)) + UsernameColour = TournamentGame.COLOUR_BLUE; + } } } } From 7d9d7066cd525ae6d20afeef6f206e9b27457730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 25 Jun 2023 17:39:15 +0200 Subject: [PATCH 0365/2100] Remove no-longer-needed BDL `permitNulls` spec --- osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index 15c37474f5..e943cb8b8c 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tournament.Components CornerRadius = 0; } - [BackgroundDependencyLoader(true)] + [BackgroundDependencyLoader] private void load(MatchIPCInfo? ipc, IAPIProvider api) { if (ipc != null) From 44a3f401887a1d1fa5f1c7076e3b694e8ce17386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 25 Jun 2023 17:39:36 +0200 Subject: [PATCH 0366/2100] Rename test step --- .../Components/TestSceneTournamentMatchChatDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs index bf59678ac5..044e5ebbbf 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tournament.Tests.Components Content = "I am a wang!" })); - AddStep("set Current match", () => ladderInfo.CurrentMatch.Value = new TournamentMatch + AddStep("set current match", () => ladderInfo.CurrentMatch.Value = new TournamentMatch { Team1 = { From 1b671b8c6e3296d373d168f643745af660a81399 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Mon, 26 Jun 2023 00:45:59 +0900 Subject: [PATCH 0367/2100] Use `GetResultSafely()` --- osu.Game.Tournament/TournamentGameBase.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 2067295eb6..7d0571dde0 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json; using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Framework.Input; @@ -245,7 +246,7 @@ namespace osu.Game.Tournament { var b = beatmapsRequiringPopulation[i]; - b.Beatmap = new TournamentBeatmap(beatmapCache.GetBeatmapAsync(b.ID).GetAwaiter().GetResult() ?? new APIBeatmap()); + b.Beatmap = new TournamentBeatmap(beatmapCache.GetBeatmapAsync(b.ID).GetResultSafely() ?? new APIBeatmap()); updateLoadProgressMessage($"Populating round beatmaps ({i} / {beatmapsRequiringPopulation.Count})"); } @@ -270,7 +271,7 @@ namespace osu.Game.Tournament { var b = beatmapsRequiringPopulation[i]; - b.Beatmap = new TournamentBeatmap(beatmapCache.GetBeatmapAsync(b.ID).GetAwaiter().GetResult() ?? new APIBeatmap()); + b.Beatmap = new TournamentBeatmap(beatmapCache.GetBeatmapAsync(b.ID).GetResultSafely() ?? new APIBeatmap()); updateLoadProgressMessage($"Populating seeding beatmaps ({i} / {beatmapsRequiringPopulation.Count})"); } From 25c9bf40614b2c89683b5415f43163baf7760b77 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 25 Jun 2023 11:22:05 -0700 Subject: [PATCH 0368/2100] Improve and refactor `LoginPanel` test scene to use `LoginOverlay` --- .../Visual/Menus/TestSceneLoginOverlay.cs | 89 +++++++++++++++++++ .../Visual/Menus/TestSceneLoginPanel.cs | 78 ---------------- osu.Game/Online/API/DummyAPIAccess.cs | 8 ++ 3 files changed, 97 insertions(+), 78 deletions(-) create mode 100644 osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs delete mode 100644 osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs new file mode 100644 index 0000000000..5c2edac84d --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs @@ -0,0 +1,89 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Overlays; +using osu.Game.Users.Drawables; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Menus +{ + [TestFixture] + public partial class TestSceneLoginOverlay : OsuManualInputManagerTestScene + { + private LoginOverlay loginOverlay = null!; + + [BackgroundDependencyLoader] + private void load() + { + Child = loginOverlay = new LoginOverlay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("show login overlay", () => loginOverlay.Show()); + } + + [Test] + public void TestLoginSuccess() + { + AddStep("logout", () => API.Logout()); + + AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password"); + AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); + } + + [Test] + public void TestLoginFailure() + { + AddStep("logout", () => + { + API.Logout(); + ((DummyAPIAccess)API).FailNextLogin(); + }); + + AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password"); + AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); + } + + [Test] + public void TestLoginConnecting() + { + AddStep("logout", () => + { + API.Logout(); + ((DummyAPIAccess)API).StayConnectingNextLogin(); + }); + + AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password"); + AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); + } + + [Test] + public void TestClickingOnFlagClosesOverlay() + { + AddStep("logout", () => API.Logout()); + AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password"); + AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); + + AddStep("click on flag", () => + { + InputManager.MoveMouseTo(loginOverlay.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("login overlay is hidden", () => loginOverlay.State.Value == Visibility.Hidden); + } + } +} diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs deleted file mode 100644 index 738220f5ce..0000000000 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using System.Linq; -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Testing; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API; -using osu.Game.Overlays.Login; -using osu.Game.Users.Drawables; -using osuTK.Input; - -namespace osu.Game.Tests.Visual.Menus -{ - [TestFixture] - public partial class TestSceneLoginPanel : OsuManualInputManagerTestScene - { - private LoginPanel loginPanel; - private int hideCount; - - [SetUpSteps] - public void SetUpSteps() - { - AddStep("create login dialog", () => - { - Add(loginPanel = new LoginPanel - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 0.5f, - RequestHide = () => hideCount++, - }); - }); - } - - [Test] - public void TestLoginSuccess() - { - AddStep("logout", () => API.Logout()); - - AddStep("enter password", () => loginPanel.ChildrenOfType().First().Text = "password"); - AddStep("submit", () => loginPanel.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); - } - - [Test] - public void TestLoginFailure() - { - AddStep("logout", () => - { - API.Logout(); - ((DummyAPIAccess)API).FailNextLogin(); - }); - - AddStep("enter password", () => loginPanel.ChildrenOfType().First().Text = "password"); - AddStep("submit", () => loginPanel.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); - } - - [Test] - public void TestClickingOnFlagClosesPanel() - { - AddStep("reset hide count", () => hideCount = 0); - - AddStep("logout", () => API.Logout()); - AddStep("enter password", () => loginPanel.ChildrenOfType().First().Text = "password"); - AddStep("submit", () => loginPanel.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); - - AddStep("click on flag", () => - { - InputManager.MoveMouseTo(loginPanel.ChildrenOfType().First()); - InputManager.Click(MouseButton.Left); - }); - AddAssert("hide requested", () => hideCount == 1); - } - } -} diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index bf9baa4414..c2ef0b4e92 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -56,6 +56,7 @@ namespace osu.Game.Online.API private readonly Bindable state = new Bindable(APIState.Online); private bool shouldFailNextLogin; + private bool stayConnectingNextLogin; /// /// The current connectivity state of the API. @@ -94,6 +95,12 @@ namespace osu.Game.Online.API { state.Value = APIState.Connecting; + if (stayConnectingNextLogin) + { + stayConnectingNextLogin = false; + return; + } + if (shouldFailNextLogin) { LastLoginError = new APIException("Not powerful enough to login.", new ArgumentException(nameof(shouldFailNextLogin))); @@ -138,6 +145,7 @@ namespace osu.Game.Online.API IBindable IAPIProvider.Activity => Activity; public void FailNextLogin() => shouldFailNextLogin = true; + public void StayConnectingNextLogin() => stayConnectingNextLogin = true; protected override void Dispose(bool isDisposing) { From 1058f434d75d74f3814c5f90756e14824decda26 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 25 Jun 2023 10:07:30 -0700 Subject: [PATCH 0369/2100] Update login overlay background to conform to other overlays --- osu.Game/Overlays/LoginOverlay.cs | 41 ++++++++++++++++--------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/osu.Game/Overlays/LoginOverlay.cs b/osu.Game/Overlays/LoginOverlay.cs index 8b60024682..b16cc5bd4b 100644 --- a/osu.Game/Overlays/LoginOverlay.cs +++ b/osu.Game/Overlays/LoginOverlay.cs @@ -4,9 +4,10 @@ #nullable disable using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; +using osu.Framework.Graphics.Effects; using osuTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Containers; @@ -21,13 +22,24 @@ namespace osu.Game.Overlays private const float transition_time = 400; + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + public LoginOverlay() { AutoSizeAxes = Axes.Both; + Masking = true; + EdgeEffect = new EdgeEffectParameters + { + Colour = Color4.Black.Opacity(0), + Type = EdgeEffectType.Shadow, + Radius = 10, + Hollow = true, + }; } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { Children = new Drawable[] { @@ -40,8 +52,7 @@ namespace osu.Game.Overlays new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.6f, + Colour = colourProvider.Background4, }, new Container { @@ -50,23 +61,11 @@ namespace osu.Game.Overlays Masking = true, AutoSizeDuration = transition_time, AutoSizeEasing = Easing.OutQuint, - Children = new Drawable[] + Child = panel = new LoginPanel { - panel = new LoginPanel - { - Padding = new MarginPadding(10), - RequestHide = Hide, - }, - new Box - { - RelativeSizeAxes = Axes.X, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Height = 3, - Colour = colours.Yellow, - Alpha = 1, - }, - } + Padding = new MarginPadding(10), + RequestHide = Hide, + }, } } } @@ -77,6 +76,7 @@ namespace osu.Game.Overlays { panel.Bounding = true; this.FadeIn(transition_time, Easing.OutQuint); + FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out); ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(panel)); } @@ -87,6 +87,7 @@ namespace osu.Game.Overlays panel.Bounding = false; this.FadeOut(transition_time); + FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.In); } } } From 442fda3598699f69853d739634473a60378389e8 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 25 Jun 2023 10:15:16 -0700 Subject: [PATCH 0370/2100] Remove using aliases --- osu.Game/Overlays/Login/LoginPanel.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 79569ada65..80238f5474 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -18,8 +19,6 @@ using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Users; using osuTK; -using RectangleF = osu.Framework.Graphics.Primitives.RectangleF; -using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Game.Overlays.Login { From 4a3b8c405eeb27ab6b7e7e7743f332d0940ab14c Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 25 Jun 2023 11:47:32 -0700 Subject: [PATCH 0371/2100] Fix login error text adding unnecessary spacing --- osu.Game/Overlays/Login/LoginForm.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index 47e706fe58..f56753dbe7 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -73,6 +73,7 @@ namespace osu.Game.Overlays.Login { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Alpha = 0, }, new SettingsCheckbox { @@ -128,7 +129,10 @@ namespace osu.Game.Overlays.Login password.OnCommit += (_, _) => performLogin(); if (api.LastLoginError?.Message is string error) + { + errorText.Alpha = 1; errorText.AddErrors(new[] { error }); + } } public override bool AcceptsFocus => true; From 59fa46bbdd6933f304521d298ef61720d3ed8929 Mon Sep 17 00:00:00 2001 From: Bastian Pedersen Date: Sun, 25 Jun 2023 21:02:41 +0200 Subject: [PATCH 0372/2100] Create localisation string class for multiplayer countdown buttons --- .../MultiplayerCountdownButtonStrings.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 osu.Game/Localisation/MultiplayerCountdownButtonStrings.cs diff --git a/osu.Game/Localisation/MultiplayerCountdownButtonStrings.cs b/osu.Game/Localisation/MultiplayerCountdownButtonStrings.cs new file mode 100644 index 0000000000..926b15386a --- /dev/null +++ b/osu.Game/Localisation/MultiplayerCountdownButtonStrings.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class MultiplayerCountdownButtonStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.MultiplayerCountdownButton"; + + /// + /// "Stop countdown" + /// + public static LocalisableString StopCountdown => new TranslatableString(getKey(@"stop_countdown"), @"Stop countdown"); + + /// + /// "Countdown settings" + /// + public static LocalisableString CountdownSettings => new TranslatableString(getKey(@"countdown_settings"), @"Countdown settings"); + + /// + /// "Start match in {0} minute/seconds" + /// + public static LocalisableString StartMatchInTime(string humanReadableTime) => new TranslatableString(getKey(@"start_match_in"), @"Start match in {0}", humanReadableTime); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} From a7153478d6e9c143cbe203a26bfeb24b0ffd47ca Mon Sep 17 00:00:00 2001 From: Bastian Pedersen Date: Sun, 25 Jun 2023 21:03:07 +0200 Subject: [PATCH 0373/2100] Use newly create localised strings for buttons --- .../Multiplayer/Match/MultiplayerCountdownButton.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs index 6dc343f00a..72c3a8122d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; using osu.Game.Online.Multiplayer; using osuTK; @@ -56,7 +57,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match base.Action = this.ShowPopover; - TooltipText = "Countdown settings"; + TooltipText = MultiplayerCountdownButtonStrings.CountdownSettings; } [BackgroundDependencyLoader] @@ -112,7 +113,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match flow.Add(new RoundedButton { RelativeSizeAxes = Axes.X, - Text = $"Start match in {duration.Humanize()}", + Text = MultiplayerCountdownButtonStrings.StartMatchInTime(duration.Humanize()), BackgroundColour = colours.Green, Action = () => { @@ -127,7 +128,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match flow.Add(new RoundedButton { RelativeSizeAxes = Axes.X, - Text = "Stop countdown", + Text = MultiplayerCountdownButtonStrings.StopCountdown, BackgroundColour = colours.Red, Action = () => { From 4582faee7988c4c0b3a49c1c47058edb39217174 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 25 Jun 2023 12:06:02 -0700 Subject: [PATCH 0374/2100] Refactor login panel to not inherit `FillFlowContainer` --- osu.Game/Overlays/Login/LoginForm.cs | 6 ++ osu.Game/Overlays/Login/LoginPanel.cs | 101 ++++++++++++-------------- 2 files changed, 53 insertions(+), 54 deletions(-) diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index f56753dbe7..55490198f8 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -11,6 +11,7 @@ using osu.Framework.Input.Events; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Overlays.Settings; @@ -56,6 +57,11 @@ namespace osu.Game.Overlays.Login Children = new Drawable[] { + new OsuSpriteText + { + Text = LoginPanelStrings.Account.ToUpper(), + Font = OsuFont.GetFont(weight: FontWeight.Bold), + }, username = new OsuTextBox { PlaceholderText = UsersStrings.LoginUsername.ToLower(), diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 80238f5474..6c09b2e6ed 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -22,7 +22,7 @@ using osuTK; namespace osu.Game.Overlays.Login { - public partial class LoginPanel : FillFlowContainer + public partial class LoginPanel : Container { private bool bounding = true; private LoginForm form; @@ -59,8 +59,6 @@ namespace osu.Game.Overlays.Login { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Direction = FillDirection.Vertical; - Spacing = new Vector2(0f, 5f); } [BackgroundDependencyLoader] @@ -77,18 +75,9 @@ namespace osu.Game.Overlays.Login switch (state.NewValue) { case APIState.Offline: - Children = new Drawable[] + Child = form = new LoginForm { - new OsuSpriteText - { - Text = LoginPanelStrings.Account.ToUpper(), - Margin = new MarginPadding { Bottom = 5 }, - Font = OsuFont.GetFont(weight: FontWeight.Bold), - }, - form = new LoginForm - { - RequestHide = RequestHide - } + RequestHide = RequestHide }; break; @@ -96,22 +85,29 @@ namespace osu.Game.Overlays.Login case APIState.Connecting: LinkFlowContainer linkFlow; - Children = new Drawable[] + Child = new FillFlowContainer { - new LoadingSpinner + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] { - State = { Value = Visibility.Visible }, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }, - linkFlow = new LinkFlowContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - TextAnchor = Anchor.TopCentre, - AutoSizeAxes = Axes.Both, - Text = state.NewValue == APIState.Failing ? ToolbarStrings.AttemptingToReconnect : ToolbarStrings.Connecting, - Margin = new MarginPadding { Top = 10, Bottom = 10 }, + new LoadingSpinner + { + State = { Value = Visibility.Visible }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + linkFlow = new LinkFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + TextAnchor = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Text = state.NewValue == APIState.Failing ? ToolbarStrings.AttemptingToReconnect : ToolbarStrings.Connecting, + Margin = new MarginPadding { Top = 10, Bottom = 10 }, + }, }, }; @@ -119,40 +115,37 @@ namespace osu.Game.Overlays.Login break; case APIState.Online: - Children = new Drawable[] + Child = new FillFlowContainer { - new FillFlowContainer + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = 20, Right = 20 }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 10f), + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = 20, Right = 20 }, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0f, 10f), - Children = new Drawable[] + new Container { - new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new[] + new OsuSpriteText { - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = LoginPanelStrings.SignedIn, - Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold), - Margin = new MarginPadding { Top = 5, Bottom = 5 }, - }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = LoginPanelStrings.SignedIn, + Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold), + Margin = new MarginPadding { Top = 5, Bottom = 5 }, }, }, - panel = new UserGridPanel(api.LocalUser.Value) - { - RelativeSizeAxes = Axes.X, - Action = RequestHide - }, - dropdown = new UserDropdown { RelativeSizeAxes = Axes.X }, }, + panel = new UserGridPanel(api.LocalUser.Value) + { + RelativeSizeAxes = Axes.X, + Action = RequestHide + }, + dropdown = new UserDropdown { RelativeSizeAxes = Axes.X }, }, }; From 671f84e32b26f6a92cdd5a3d82b0215d8b65f708 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 25 Jun 2023 12:15:03 -0700 Subject: [PATCH 0375/2100] Remove unnecessary container --- osu.Game/Overlays/Login/LoginPanel.cs | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 6c09b2e6ed..885f5639a7 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -124,21 +124,13 @@ namespace osu.Game.Overlays.Login Spacing = new Vector2(0f, 10f), Children = new Drawable[] { - new Container + new OsuSpriteText { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new[] - { - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = LoginPanelStrings.SignedIn, - Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold), - Margin = new MarginPadding { Top = 5, Bottom = 5 }, - }, - }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = LoginPanelStrings.SignedIn, + Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold), + Margin = new MarginPadding { Top = 5, Bottom = 5 }, }, panel = new UserGridPanel(api.LocalUser.Value) { From 6ebc2581c2a207fe45ee1b6cac2adb2a9bfe04cf Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 25 Jun 2023 12:26:01 -0700 Subject: [PATCH 0376/2100] Normalise login overlay padding/spacing --- osu.Game/Overlays/Login/LoginForm.cs | 67 +++++++++++++------------ osu.Game/Overlays/Login/LoginPanel.cs | 10 ++-- osu.Game/Overlays/Login/UserDropdown.cs | 3 -- osu.Game/Overlays/LoginOverlay.cs | 3 +- 4 files changed, 42 insertions(+), 41 deletions(-) diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index 55490198f8..0eef55162f 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -43,43 +43,50 @@ namespace osu.Game.Overlays.Login [BackgroundDependencyLoader(permitNulls: true)] private void load(OsuConfigManager config, AccountCreationOverlay accountCreation) { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; Direction = FillDirection.Vertical; Spacing = new Vector2(0, SettingsSection.ITEM_SPACING); - AutoSizeAxes = Axes.Y; - RelativeSizeAxes = Axes.X; - Padding = new MarginPadding - { - Top = 5, - Bottom = 24, - }; + ErrorTextFlowContainer errorText; LinkFlowContainer forgottenPasswordLink; Children = new Drawable[] { - new OsuSpriteText - { - Text = LoginPanelStrings.Account.ToUpper(), - Font = OsuFont.GetFont(weight: FontWeight.Bold), - }, - username = new OsuTextBox - { - PlaceholderText = UsersStrings.LoginUsername.ToLower(), - RelativeSizeAxes = Axes.X, - Text = api.ProvidedUsername, - TabbableContentContainer = this - }, - password = new OsuPasswordTextBox - { - PlaceholderText = UsersStrings.LoginPassword.ToLower(), - RelativeSizeAxes = Axes.X, - TabbableContentContainer = this, - }, - errorText = new ErrorTextFlowContainer + new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Alpha = 0, + Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, SettingsSection.ITEM_SPACING), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = LoginPanelStrings.Account.ToUpper(), + Font = OsuFont.GetFont(weight: FontWeight.Bold), + }, + username = new OsuTextBox + { + PlaceholderText = UsersStrings.LoginUsername.ToLower(), + RelativeSizeAxes = Axes.X, + Text = api.ProvidedUsername, + TabbableContentContainer = this + }, + password = new OsuPasswordTextBox + { + PlaceholderText = UsersStrings.LoginPassword.ToLower(), + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + }, + errorText = new ErrorTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + }, + }, }, new SettingsCheckbox { @@ -93,11 +100,7 @@ namespace osu.Game.Overlays.Login }, forgottenPasswordLink = new LinkFlowContainer { - Padding = new MarginPadding - { - Left = SettingsPanel.CONTENT_MARGINS, - Bottom = SettingsSection.ITEM_SPACING - }, + Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, }, diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 885f5639a7..a84a211dfc 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -17,6 +17,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Online.API; +using osu.Game.Overlays.Settings; using osu.Game.Users; using osuTK; @@ -89,8 +90,9 @@ namespace osu.Game.Overlays.Login { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }, Direction = FillDirection.Vertical, - Spacing = new Vector2(0f, 5f), + Spacing = new Vector2(0f, SettingsSection.ITEM_SPACING), Children = new Drawable[] { new LoadingSpinner @@ -106,7 +108,6 @@ namespace osu.Game.Overlays.Login TextAnchor = Anchor.TopCentre, AutoSizeAxes = Axes.Both, Text = state.NewValue == APIState.Failing ? ToolbarStrings.AttemptingToReconnect : ToolbarStrings.Connecting, - Margin = new MarginPadding { Top = 10, Bottom = 10 }, }, }, }; @@ -119,9 +120,9 @@ namespace osu.Game.Overlays.Login { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = 20, Right = 20 }, + Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }, Direction = FillDirection.Vertical, - Spacing = new Vector2(0f, 10f), + Spacing = new Vector2(0f, SettingsSection.ITEM_SPACING), Children = new Drawable[] { new OsuSpriteText @@ -130,7 +131,6 @@ namespace osu.Game.Overlays.Login Origin = Anchor.TopCentre, Text = LoginPanelStrings.SignedIn, Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold), - Margin = new MarginPadding { Top = 5, Bottom = 5 }, }, panel = new UserGridPanel(api.LocalUser.Value) { diff --git a/osu.Game/Overlays/Login/UserDropdown.cs b/osu.Game/Overlays/Login/UserDropdown.cs index 78b5271ca0..9ffaaff8bf 100644 --- a/osu.Game/Overlays/Login/UserDropdown.cs +++ b/osu.Game/Overlays/Login/UserDropdown.cs @@ -36,8 +36,6 @@ namespace osu.Game.Overlays.Login Masking = true; CornerRadius = 5; - Margin = new MarginPadding { Bottom = 5 }; - EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, @@ -86,7 +84,6 @@ namespace osu.Game.Overlays.Login public UserDropdownHeader() { Foreground.Padding = new MarginPadding { Left = 10, Right = 10 }; - Margin = new MarginPadding { Bottom = 5 }; Masking = true; CornerRadius = 5; EdgeEffect = new EdgeEffectParameters diff --git a/osu.Game/Overlays/LoginOverlay.cs b/osu.Game/Overlays/LoginOverlay.cs index b16cc5bd4b..ecf2e7a634 100644 --- a/osu.Game/Overlays/LoginOverlay.cs +++ b/osu.Game/Overlays/LoginOverlay.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Overlays.Login; +using osu.Game.Overlays.Settings; namespace osu.Game.Overlays { @@ -63,7 +64,7 @@ namespace osu.Game.Overlays AutoSizeEasing = Easing.OutQuint, Child = panel = new LoginPanel { - Padding = new MarginPadding(10), + Padding = new MarginPadding { Vertical = SettingsSection.ITEM_SPACING }, RequestHide = Hide, }, } From ccc4d1609617e20fed35c212fd0782b7a6e43230 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 25 Jun 2023 12:46:00 -0700 Subject: [PATCH 0377/2100] Remove most custom styling of user dropdown --- osu.Game/Overlays/Login/UserDropdown.cs | 51 +------------------------ 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/osu.Game/Overlays/Login/UserDropdown.cs b/osu.Game/Overlays/Login/UserDropdown.cs index 9ffaaff8bf..f0449d50b5 100644 --- a/osu.Game/Overlays/Login/UserDropdown.cs +++ b/osu.Game/Overlays/Login/UserDropdown.cs @@ -31,27 +31,6 @@ namespace osu.Game.Overlays.Login protected partial class UserDropdownMenu : OsuDropdownMenu { - public UserDropdownMenu() - { - Masking = true; - CornerRadius = 5; - - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.25f), - Radius = 4, - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - BackgroundColour = colours.Gray3; - SelectionColour = colours.Gray4; - HoverColour = colours.Gray5; - } - protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableUserDropdownMenuItem(item); private partial class DrawableUserDropdownMenuItem : DrawableOsuDropdownMenuItem @@ -60,20 +39,12 @@ namespace osu.Game.Overlays.Login : base(item) { Foreground.Padding = new MarginPadding { Top = 5, Bottom = 5, Left = 10, Right = 5 }; - CornerRadius = 5; } - - protected override Drawable CreateContent() => new Content - { - Label = { Margin = new MarginPadding { Left = UserDropdownHeader.LABEL_LEFT_MARGIN - 11 } } - }; } } private partial class UserDropdownHeader : OsuDropdownHeader { - public const float LABEL_LEFT_MARGIN = 20; - private readonly StatusIcon statusIcon; public Color4 StatusColour @@ -83,19 +54,6 @@ namespace osu.Game.Overlays.Login public UserDropdownHeader() { - Foreground.Padding = new MarginPadding { Left = 10, Right = 10 }; - Masking = true; - CornerRadius = 5; - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.25f), - Radius = 4, - }; - - Icon.Size = new Vector2(14); - Icon.Margin = new MarginPadding(0); - Foreground.Add(statusIcon = new StatusIcon { Anchor = Anchor.CentreLeft, @@ -103,14 +61,7 @@ namespace osu.Game.Overlays.Login Size = new Vector2(14), }); - Text.Margin = new MarginPadding { Left = LABEL_LEFT_MARGIN }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - BackgroundColour = colours.Gray3; - BackgroundColourHover = colours.Gray5; + Text.Margin = new MarginPadding { Left = 20 }; } } } From 5477ef6bfb7a253ed1bc93356c48de7ceb218bcb Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 25 Jun 2023 12:58:51 -0700 Subject: [PATCH 0378/2100] Remove unused usings --- osu.Game/Overlays/Login/UserDropdown.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Overlays/Login/UserDropdown.cs b/osu.Game/Overlays/Login/UserDropdown.cs index f0449d50b5..f2a12f9a1e 100644 --- a/osu.Game/Overlays/Login/UserDropdown.cs +++ b/osu.Game/Overlays/Login/UserDropdown.cs @@ -1,12 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Users.Drawables; using osuTK; From 8d2dccbda56eb768f0255a6746fccdd56de73eec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Jun 2023 13:19:07 +0900 Subject: [PATCH 0379/2100] Remove pointless zero opacity specification --- osu.Game/Overlays/LoginOverlay.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/LoginOverlay.cs b/osu.Game/Overlays/LoginOverlay.cs index ecf2e7a634..de37aaa291 100644 --- a/osu.Game/Overlays/LoginOverlay.cs +++ b/osu.Game/Overlays/LoginOverlay.cs @@ -4,7 +4,6 @@ #nullable disable using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; @@ -32,7 +31,7 @@ namespace osu.Game.Overlays Masking = true; EdgeEffect = new EdgeEffectParameters { - Colour = Color4.Black.Opacity(0), + Colour = Color4.Black, Type = EdgeEffectType.Shadow, Radius = 10, Hollow = true, From ac0c988d4932f4e5efe826963188b0e17a705247 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Jun 2023 13:21:29 +0900 Subject: [PATCH 0380/2100] Fix weirdly named test method and add xmldoc --- osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs | 2 +- osu.Game/Online/API/DummyAPIAccess.cs | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs index 5c2edac84d..0bc71924ce 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("logout", () => { API.Logout(); - ((DummyAPIAccess)API).StayConnectingNextLogin(); + ((DummyAPIAccess)API).PauseOnConnectingNextLogin(); }); AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password"); diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index c2ef0b4e92..45a4737c21 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -144,8 +144,15 @@ namespace osu.Game.Online.API IBindableList IAPIProvider.Friends => Friends; IBindable IAPIProvider.Activity => Activity; + /// + /// During the next simulated login, the process will fail immediately. + /// public void FailNextLogin() => shouldFailNextLogin = true; - public void StayConnectingNextLogin() => stayConnectingNextLogin = true; + + /// + /// During the next simulated login, the process will pause indefinitely at "connecting". + /// + public void PauseOnConnectingNextLogin() => stayConnectingNextLogin = true; protected override void Dispose(bool isDisposing) { From 1abce098b4876946690ae69c69002127a0f0b070 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Jun 2023 13:26:07 +0900 Subject: [PATCH 0381/2100] Apply nullability to login form related classes --- osu.Game/Online/API/DummyAPIAccess.cs | 10 ++++------ osu.Game/Overlays/Login/LoginPanel.cs | 15 +++++++-------- osu.Game/Overlays/LoginOverlay.cs | 4 +--- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 45a4737c21..01169828b0 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Threading; using System.Threading.Tasks; @@ -45,13 +43,13 @@ namespace osu.Game.Online.API public int APIVersion => int.Parse(DateTime.Now.ToString("yyyyMMdd")); - public Exception LastLoginError { get; private set; } + public Exception? LastLoginError { get; private set; } /// /// Provide handling logic for an arbitrary API request. /// Should return true is a request was handled. If null or false return, the request will be failed with a . /// - public Func HandleRequest; + public Func? HandleRequest; private readonly Bindable state = new Bindable(APIState.Online); @@ -128,11 +126,11 @@ namespace osu.Game.Online.API LocalUser.Value = new GuestUser(); } - public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null; + public IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null; public NotificationsClientConnector GetNotificationsConnector() => new PollingNotificationsClientConnector(this); - public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) + public RegistrationRequest.RegistrationRequestErrors? CreateAccount(string email, string username, string password) { Thread.Sleep(200); return null; diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index a84a211dfc..71ecf2e75a 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -26,23 +24,24 @@ namespace osu.Game.Overlays.Login public partial class LoginPanel : Container { private bool bounding = true; - private LoginForm form; + + private LoginForm? form; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; - private UserGridPanel panel; - private UserDropdown dropdown; + private UserGridPanel panel = null!; + private UserDropdown dropdown = null!; /// /// Called to request a hide of a parent displaying this container. /// - public Action RequestHide; + public Action? RequestHide; private readonly IBindable apiState = new Bindable(); [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; public override RectangleF BoundingBox => bounding ? base.BoundingBox : RectangleF.Empty; diff --git a/osu.Game/Overlays/LoginOverlay.cs b/osu.Game/Overlays/LoginOverlay.cs index de37aaa291..a575253e71 100644 --- a/osu.Game/Overlays/LoginOverlay.cs +++ b/osu.Game/Overlays/LoginOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,7 +16,7 @@ namespace osu.Game.Overlays { public partial class LoginOverlay : OsuFocusedOverlayContainer { - private LoginPanel panel; + private LoginPanel panel = null!; private const float transition_time = 400; From b240ce295b76c91ee6c2c2839ab2173e16eea75c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Jun 2023 13:38:34 +0900 Subject: [PATCH 0382/2100] Rename class and key to better match expectations --- ...untdownButtonStrings.cs => MultiplayerMatchStrings.cs} | 8 ++++---- .../Multiplayer/Match/MultiplayerCountdownButton.cs | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) rename osu.Game/Localisation/{MultiplayerCountdownButtonStrings.cs => MultiplayerMatchStrings.cs} (72%) diff --git a/osu.Game/Localisation/MultiplayerCountdownButtonStrings.cs b/osu.Game/Localisation/MultiplayerMatchStrings.cs similarity index 72% rename from osu.Game/Localisation/MultiplayerCountdownButtonStrings.cs rename to osu.Game/Localisation/MultiplayerMatchStrings.cs index 926b15386a..95c7168a09 100644 --- a/osu.Game/Localisation/MultiplayerCountdownButtonStrings.cs +++ b/osu.Game/Localisation/MultiplayerMatchStrings.cs @@ -5,9 +5,9 @@ using osu.Framework.Localisation; namespace osu.Game.Localisation { - public static class MultiplayerCountdownButtonStrings + public static class MultiplayerMatchStrings { - private const string prefix = @"osu.Game.Resources.Localisation.MultiplayerCountdownButton"; + private const string prefix = @"osu.Game.Resources.Localisation.MultiplayerMatchStrings"; /// /// "Stop countdown" @@ -20,9 +20,9 @@ namespace osu.Game.Localisation public static LocalisableString CountdownSettings => new TranslatableString(getKey(@"countdown_settings"), @"Countdown settings"); /// - /// "Start match in {0} minute/seconds" + /// "Start match in {0}" /// - public static LocalisableString StartMatchInTime(string humanReadableTime) => new TranslatableString(getKey(@"start_match_in"), @"Start match in {0}", humanReadableTime); + public static LocalisableString StartMatchWithCountdown(string humanReadableTime) => new TranslatableString(getKey(@"start_match_width_countdown"), @"Start match in {0}", humanReadableTime); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs index 72c3a8122d..e1543eaceb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match base.Action = this.ShowPopover; - TooltipText = MultiplayerCountdownButtonStrings.CountdownSettings; + TooltipText = MultiplayerMatchStrings.CountdownSettings; } [BackgroundDependencyLoader] @@ -113,7 +113,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match flow.Add(new RoundedButton { RelativeSizeAxes = Axes.X, - Text = MultiplayerCountdownButtonStrings.StartMatchInTime(duration.Humanize()), + Text = MultiplayerMatchStrings.StartMatchWithCountdown(duration.Humanize()), BackgroundColour = colours.Green, Action = () => { @@ -128,7 +128,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match flow.Add(new RoundedButton { RelativeSizeAxes = Axes.X, - Text = MultiplayerCountdownButtonStrings.StopCountdown, + Text = MultiplayerMatchStrings.StopCountdown, BackgroundColour = colours.Red, Action = () => { From ba7f4722478bb0ac4e84962f2946269fbed9fb4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Jun 2023 16:04:16 +0900 Subject: [PATCH 0383/2100] Split padding out into constant to fix weird looking math --- osu.Game/Skinning/LegacySkin.cs | 4 +++- osu.Game/Skinning/TrianglesSkin.cs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index e264af4c83..79f13686e8 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -383,9 +383,11 @@ namespace osu.Game.Skinning if (keyCounter != null) { + const float padding = 10; + keyCounter.Anchor = Anchor.BottomRight; keyCounter.Origin = Anchor.BottomRight; - keyCounter.Position = new Vector2(-10, -10 - hitError.Width); + keyCounter.Position = new Vector2(-padding, -(padding + hitError.Width)); } } }) diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index 5f839fad0b..a4cfef63d4 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -146,9 +146,11 @@ namespace osu.Game.Skinning if (songProgress != null && keyCounter != null) { + const float padding = 10; + keyCounter.Anchor = Anchor.BottomRight; keyCounter.Origin = Anchor.BottomRight; - keyCounter.Position = new Vector2(-10, -60 - 10); + keyCounter.Position = new Vector2(-padding, -(60 + padding)); } }) { From 52fdeeb4911c390a826aef8d6b15378bc8ae0d2e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Jun 2023 16:20:51 +0900 Subject: [PATCH 0384/2100] Improve positioning and positioning code clarity for argon / triangles implementations --- osu.Game/Skinning/ArgonSkin.cs | 11 ++++++++--- osu.Game/Skinning/TrianglesSkin.cs | 5 ++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 48326bfe60..3570922a9e 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -168,14 +168,19 @@ namespace osu.Game.Skinning if (songProgress != null) { - songProgress.Position = new Vector2(0, -10); + const float padding = 10; + + songProgress.Position = new Vector2(0, -padding); songProgress.Scale = new Vector2(0.9f, 1); - if (keyCounter != null) + if (keyCounter != null && hitError != null) { + // Hard to find this at runtime, so taken from the most expanded state during replay. + const float song_progress_offset_height = 36 + padding; + keyCounter.Anchor = Anchor.BottomLeft; keyCounter.Origin = Anchor.BottomLeft; - keyCounter.Position = new Vector2(50, -57); + keyCounter.Position = new Vector2(hitError.Width + padding, -(padding * 2 + song_progress_offset_height)); } } } diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index a4cfef63d4..a68a7fd5b9 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -148,9 +148,12 @@ namespace osu.Game.Skinning { const float padding = 10; + // Hard to find this at runtime, so taken from the most expanded state during replay. + const float song_progress_offset_height = 73; + keyCounter.Anchor = Anchor.BottomRight; keyCounter.Origin = Anchor.BottomRight; - keyCounter.Position = new Vector2(-padding, -(60 + padding)); + keyCounter.Position = new Vector2(-padding, -(song_progress_offset_height + padding)); } }) { From 3a2dd0e7dd2eb5b19807194037c9751db354dbb2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Jun 2023 16:22:38 +0900 Subject: [PATCH 0385/2100] Move argon key counter to right to match stable expectations --- osu.Game/Skinning/ArgonSkin.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 3570922a9e..ba392386de 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -178,9 +178,9 @@ namespace osu.Game.Skinning // Hard to find this at runtime, so taken from the most expanded state during replay. const float song_progress_offset_height = 36 + padding; - keyCounter.Anchor = Anchor.BottomLeft; - keyCounter.Origin = Anchor.BottomLeft; - keyCounter.Position = new Vector2(hitError.Width + padding, -(padding * 2 + song_progress_offset_height)); + keyCounter.Anchor = Anchor.BottomRight; + keyCounter.Origin = Anchor.BottomRight; + keyCounter.Position = new Vector2(-(hitError.Width + padding), -(padding * 2 + song_progress_offset_height)); } } } From ec209428300ee776d515daaffd73059248e70f1c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Jun 2023 16:24:36 +0900 Subject: [PATCH 0386/2100] Actuall add composite component to hierarchy --- osu.Game/Screens/Play/HUDOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 21636ac04c..9f3b7d5a93 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -116,6 +116,7 @@ namespace osu.Game.Screens.Play CreateFailingLayer(), //Needs to be initialized before skinnable drawables. tally = new JudgementTally(), + KeyCounter = new KeyCounterController(), mainComponents = new HUDComponentsContainer { AlwaysPresent = true, }, rulesetComponents = drawableRuleset != null ? new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, } @@ -159,7 +160,6 @@ namespace osu.Game.Screens.Play }, clicksPerSecondCalculator = new ClicksPerSecondCalculator(), }; - KeyCounter = new KeyCounterController(); hideTargets = new List { mainComponents, rulesetComponents, topRightElements }; From c8e081c2b6883c62fcf0cb5e8ceada3acff4b80c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Jun 2023 16:32:04 +0900 Subject: [PATCH 0387/2100] Remove unused interface --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 4 ---- .../Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs | 2 +- osu.Game/Screens/Play/HUD/KeyCounterController.cs | 3 +-- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 2c403139b6..a8a5962762 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -248,10 +248,6 @@ namespace osu.Game.Rulesets.UI void Attach(ClicksPerSecondCalculator calculator); } - public interface IAttachableSkinComponent - { - } - public class RulesetInputManagerInputState : InputState where T : struct { diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs index 3e55e11f1c..ba0c47dc8b 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Play.HUD.ClicksPerSecond { - public partial class ClicksPerSecondCalculator : Component, IAttachableSkinComponent + public partial class ClicksPerSecondCalculator : Component { private readonly List timestamps = new List(); diff --git a/osu.Game/Screens/Play/HUD/KeyCounterController.cs b/osu.Game/Screens/Play/HUD/KeyCounterController.cs index 0fa02afbb4..f64a0306c6 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterController.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterController.cs @@ -6,11 +6,10 @@ using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Play.HUD { - public partial class KeyCounterController : CompositeComponent, IAttachableSkinComponent + public partial class KeyCounterController : CompositeComponent { public readonly Bindable IsCounting = new BindableBool(true); From 084354a8dc5a3ce266528571340f3180909c4efe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Jun 2023 16:34:57 +0900 Subject: [PATCH 0388/2100] Split out interfaces from `RulesetInputManager` and improve xmldoc --- osu.Game/Rulesets/UI/ICanAttachHUDPieces.cs | 21 ++++++++++++++++++ osu.Game/Rulesets/UI/IHasRecordingHandler.cs | 15 +++++++++++++ osu.Game/Rulesets/UI/IHasReplayHandler.cs | 16 ++++++++++++++ osu.Game/Rulesets/UI/RulesetInputManager.cs | 23 -------------------- 4 files changed, 52 insertions(+), 23 deletions(-) create mode 100644 osu.Game/Rulesets/UI/ICanAttachHUDPieces.cs create mode 100644 osu.Game/Rulesets/UI/IHasRecordingHandler.cs create mode 100644 osu.Game/Rulesets/UI/IHasReplayHandler.cs diff --git a/osu.Game/Rulesets/UI/ICanAttachHUDPieces.cs b/osu.Game/Rulesets/UI/ICanAttachHUDPieces.cs new file mode 100644 index 0000000000..b81af8556e --- /dev/null +++ b/osu.Game/Rulesets/UI/ICanAttachHUDPieces.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. + +using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Play.HUD.ClicksPerSecond; + +namespace osu.Game.Rulesets.UI +{ + /// + /// A target (generally always ) which can attach various skinnable components. + /// + /// + /// Attach methods will give the target permission to prepare the component into a usable state, usually via + /// calling methods on the component (attaching various gameplay devices). + /// + public interface ICanAttachHUDPieces + { + void Attach(KeyCounterController keyCounter); + void Attach(ClicksPerSecondCalculator calculator); + } +} diff --git a/osu.Game/Rulesets/UI/IHasRecordingHandler.cs b/osu.Game/Rulesets/UI/IHasRecordingHandler.cs new file mode 100644 index 0000000000..2a13272a98 --- /dev/null +++ b/osu.Game/Rulesets/UI/IHasRecordingHandler.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Input; + +namespace osu.Game.Rulesets.UI +{ + /// + /// Expose the in a capable . + /// + public interface IHasRecordingHandler + { + public ReplayRecorder Recorder { set; } + } +} diff --git a/osu.Game/Rulesets/UI/IHasReplayHandler.cs b/osu.Game/Rulesets/UI/IHasReplayHandler.cs new file mode 100644 index 0000000000..561c582b71 --- /dev/null +++ b/osu.Game/Rulesets/UI/IHasReplayHandler.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Input; +using osu.Game.Input.Handlers; + +namespace osu.Game.Rulesets.UI +{ + /// + /// Expose the in a capable . + /// + public interface IHasReplayHandler + { + ReplayInputHandler? ReplayInputHandler { get; set; } + } +} diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index a8a5962762..9f4fabc300 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -225,29 +225,6 @@ namespace osu.Game.Rulesets.UI } } - /// - /// Expose the in a capable . - /// - public interface IHasReplayHandler - { - ReplayInputHandler ReplayInputHandler { get; set; } - } - - public interface IHasRecordingHandler - { - public ReplayRecorder Recorder { set; } - } - - /// - /// Supports attaching various HUD pieces. - /// Keys will be populated automatically and a receptor will be injected inside. - /// - public interface ICanAttachHUDPieces - { - void Attach(KeyCounterController keyCounter); - void Attach(ClicksPerSecondCalculator calculator); - } - public class RulesetInputManagerInputState : InputState where T : struct { From 44c08f3944854cd1bcd5c7ac1469d1b8cd637154 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Jun 2023 16:47:33 +0900 Subject: [PATCH 0389/2100] Add xmldoc for `KeyCounterController` --- osu.Game/Screens/Play/HUD/KeyCounterController.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/KeyCounterController.cs b/osu.Game/Screens/Play/HUD/KeyCounterController.cs index f64a0306c6..93e0528e6b 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterController.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterController.cs @@ -9,6 +9,10 @@ using osu.Framework.Graphics.Containers; namespace osu.Game.Screens.Play.HUD { + /// + /// Keeps track of key press counts for a current play session, exposing bindable counts which can + /// be used for display purposes. + /// public partial class KeyCounterController : CompositeComponent { public readonly Bindable IsCounting = new BindableBool(true); From 14c95f45848c90457425fc85b302517d82db3968 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Jun 2023 17:28:08 +0900 Subject: [PATCH 0390/2100] Apply NRT to `FilterCriteria` --- .../SongSelect/TestSceneFilterControl.cs | 2 +- osu.Game/Screens/Select/FilterCriteria.cs | 18 +++++++----------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 64e2447cca..f020cd02f7 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -210,7 +210,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Click(MouseButton.Left); }); - AddAssert("collection filter still selected", () => control.CreateCriteria().CollectionBeatmapMD5Hashes.Any()); + AddAssert("collection filter still selected", () => control.CreateCriteria().CollectionBeatmapMD5Hashes?.Any() ?? false); AddAssert("filter request not fired", () => !received); } diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 320bfb1b45..2be8c71e21 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -1,12 +1,10 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +#nullable enable +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Rulesets; @@ -20,7 +18,7 @@ namespace osu.Game.Screens.Select public GroupMode Group; public SortMode Sort; - public BeatmapSetInfo SelectedBeatmapSet; + public BeatmapSetInfo? SelectedBeatmapSet; public OptionalRange StarDifficulty; public OptionalRange ApproachRate; @@ -42,10 +40,10 @@ namespace osu.Game.Screens.Select public string[] SearchTerms = Array.Empty(); - public RulesetInfo Ruleset; + public RulesetInfo? Ruleset; public bool AllowConvertedBeatmaps; - private string searchText; + private string searchText = string.Empty; /// /// as a number (if it can be parsed as one). @@ -70,11 +68,9 @@ namespace osu.Game.Screens.Select /// /// Hashes from the to filter to. /// - [CanBeNull] - public IEnumerable CollectionBeatmapMD5Hashes { get; set; } + public IEnumerable? CollectionBeatmapMD5Hashes { get; set; } - [CanBeNull] - public IRulesetFilterCriteria RulesetCriteria { get; set; } + public IRulesetFilterCriteria? RulesetCriteria { get; set; } public struct OptionalRange : IEquatable> where T : struct From 1960cd08397ed18c67695391b70e0ce415e92fc4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Jun 2023 18:26:39 +0900 Subject: [PATCH 0391/2100] Add test coverage of exact matching of search terms --- .../NonVisual/Filtering/FilterMatchingTest.cs | 24 +++++++++++++++++ .../Filtering/FilterQueryParserTest.cs | 27 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index 33204d33a7..8bd9407599 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -159,6 +159,30 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual(filtered, carouselItem.Filtered.Value); } + [Test] + [TestCase("\"artist\"", false)] + [TestCase("\"arti\"", true)] + [TestCase("\"artist title author\"", true)] + [TestCase("\"artist\" \"title\" \"author\"", false)] + [TestCase("\"an artist\"", true)] + [TestCase("\"tags too\"", false)] + [TestCase("\"tags to\"", true)] + [TestCase("\"version\"", false)] + [TestCase("\"an auteur\"", true)] + public void TestCriteriaMatchingExactTerms(string terms, bool filtered) + { + var exampleBeatmapInfo = getExampleBeatmap(); + var criteria = new FilterCriteria + { + Ruleset = new RulesetInfo { OnlineID = 6 }, + AllowConvertedBeatmaps = true, + SearchText = terms + }; + var carouselItem = new CarouselBeatmap(exampleBeatmapInfo); + carouselItem.Filter(criteria); + Assert.AreEqual(filtered, carouselItem.Filtered.Value); + } + [Test] [TestCase("", false)] [TestCase("The", false)] diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index da32edb8fb..46f91f9a67 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -23,6 +23,31 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual(4, filterCriteria.SearchTerms.Length); } + [Test] + public void TestApplyQueriesBareWordsWithExactMatch() + { + const string query = "looking for \"a beatmap\" like \"this\""; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("looking for \"a beatmap\" like \"this\"", filterCriteria.SearchText); + Assert.AreEqual(5, filterCriteria.SearchTerms.Length); + + Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("a beatmap")); + Assert.That(filterCriteria.SearchTerms[0].Exact, Is.True); + + Assert.That(filterCriteria.SearchTerms[1].SearchTerm, Is.EqualTo("this")); + Assert.That(filterCriteria.SearchTerms[1].Exact, Is.True); + + Assert.That(filterCriteria.SearchTerms[2].SearchTerm, Is.EqualTo("looking")); + Assert.That(filterCriteria.SearchTerms[2].Exact, Is.False); + + Assert.That(filterCriteria.SearchTerms[3].SearchTerm, Is.EqualTo("for")); + Assert.That(filterCriteria.SearchTerms[3].Exact, Is.False); + + Assert.That(filterCriteria.SearchTerms[4].SearchTerm, Is.EqualTo("like")); + Assert.That(filterCriteria.SearchTerms[4].Exact, Is.False); + } + /* * The following tests have been written a bit strangely (they don't check exact * bound equality with what the filter says). @@ -235,6 +260,7 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual("find me songs by please", filterCriteria.SearchText.Trim()); Assert.AreEqual(5, filterCriteria.SearchTerms.Length); Assert.AreEqual("singer", filterCriteria.Artist.SearchTerm); + Assert.That(filterCriteria.Artist.Exact, Is.False); } [Test] @@ -246,6 +272,7 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual("really like yes", filterCriteria.SearchText.Trim()); Assert.AreEqual(3, filterCriteria.SearchTerms.Length); Assert.AreEqual("name with space", filterCriteria.Artist.SearchTerm); + Assert.That(filterCriteria.Artist.Exact, Is.True); } [Test] From a74547c43cf0328d69ec5671635f2a4a4c05356a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Jun 2023 17:30:08 +0900 Subject: [PATCH 0392/2100] Add exact match support at song select --- .../Select/Carousel/CarouselBeatmap.cs | 10 ++-- osu.Game/Screens/Select/FilterCriteria.cs | 47 +++++++++++++++++-- osu.Game/Screens/Select/FilterQueryParser.cs | 2 +- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 7e48bc5cdd..18931c462f 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -1,7 +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.Linq; using osu.Game.Beatmaps; using osu.Game.Screens.Select.Filter; @@ -65,16 +64,16 @@ namespace osu.Game.Screens.Select.Carousel if (criteria.SearchTerms.Length > 0) { - var terms = BeatmapInfo.GetSearchableTerms(); + var searchableTerms = BeatmapInfo.GetSearchableTerms(); - foreach (string criteriaTerm in criteria.SearchTerms) + foreach (FilterCriteria.OptionalTextFilter criteriaTerm in criteria.SearchTerms) { bool any = false; // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator - foreach (string term in terms) + foreach (string searchTerm in searchableTerms) { - if (!term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase)) continue; + if (!criteriaTerm.Matches(searchTerm)) continue; any = true; break; @@ -98,7 +97,6 @@ namespace osu.Game.Screens.Select.Carousel if (!match) return false; match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true; - if (match && criteria.RulesetCriteria != null) match &= criteria.RulesetCriteria.Matches(BeatmapInfo); diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 2be8c71e21..3fcc8d5476 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -1,10 +1,14 @@ -#nullable enable +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable // 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.Text.RegularExpressions; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Rulesets; @@ -38,7 +42,7 @@ namespace osu.Game.Screens.Select IsUpperInclusive = true }; - public string[] SearchTerms = Array.Empty(); + public OptionalTextFilter[] SearchTerms = Array.Empty(); public RulesetInfo? Ruleset; public bool AllowConvertedBeatmaps; @@ -56,11 +60,29 @@ namespace osu.Game.Screens.Select set { searchText = value; - SearchTerms = searchText.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray(); + + List terms = new List(); + + string remainingText = value; + + // First handle quoted segments to ensure we keep inline spaces in exact matches. + foreach (Match quotedSegment in Regex.Matches(searchText, "(\"[^\"]+\")")) + { + terms.Add(new OptionalTextFilter { SearchTerm = quotedSegment.Value }); + remainingText = remainingText.Replace(quotedSegment.Value, string.Empty); + } + + // Then handle the rest splitting on any spaces. + terms.AddRange(remainingText.Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(s => new OptionalTextFilter + { + SearchTerm = s + })); + + SearchTerms = terms.ToArray(); SearchNumber = null; - if (SearchTerms.Length == 1 && int.TryParse(SearchTerms[0], out int parsed)) + if (SearchTerms.Length == 1 && int.TryParse(SearchTerms[0].SearchTerm, out int parsed)) SearchNumber = parsed; } } @@ -120,6 +142,8 @@ namespace osu.Game.Screens.Select { public bool HasFilter => !string.IsNullOrEmpty(SearchTerm); + public bool Exact { get; private set; } + public bool Matches(string value) { if (!HasFilter) @@ -129,10 +153,23 @@ namespace osu.Game.Screens.Select if (string.IsNullOrEmpty(value)) return false; + if (Exact) + return Regex.IsMatch(value, $@"(^|\s){searchTerm}($|\s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase); } - public string SearchTerm; + private string searchTerm; + + public string SearchTerm + { + get => searchTerm; + set + { + searchTerm = value.Trim('"'); + Exact = searchTerm != value; + } + } public bool Equals(OptionalTextFilter other) => SearchTerm == other.SearchTerm; } diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index c86554ddbc..474a9fdfea 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -161,7 +161,7 @@ namespace osu.Game.Screens.Select switch (op) { case Operator.Equal: - textFilter.SearchTerm = value.Trim('"'); + textFilter.SearchTerm = value; return true; default: From 8e79510793775ebab730d68d5fd962298aa77aa3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 26 Jun 2023 17:52:47 +0900 Subject: [PATCH 0393/2100] Add migration for total score conversion --- osu.Game/Database/RealmAccess.cs | 13 ++++++++++++- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 3 ++- osu.Game/Scoring/ScoreInfo.cs | 3 +++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index da4caa42ba..e3423d25c5 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -78,8 +78,9 @@ namespace osu.Game.Database /// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files. /// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes. /// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations. + /// 31 2023-06-26 Add Version to ScoreInfo, set to 30000002. /// - private const int schema_version = 30; + private const int schema_version = 31; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -966,6 +967,16 @@ namespace osu.Game.Database break; } + + case 31: + { + var scores = migration.NewRealm.All(); + + foreach (var score in scores) + score.Version = 30000002; // Last version before legacy total score conversion. + + break; + } } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index f71da6c7e0..6c8b99b842 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -29,9 +29,10 @@ namespace osu.Game.Scoring.Legacy /// /// 30000001: Appends to the end of scores. /// 30000002: Score stored to replay calculated using the Score V2 algorithm. + /// 30000003: First version after legacy total score migration. /// /// - public const int LATEST_VERSION = 30000002; + public const int LATEST_VERSION = 30000003; /// /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index d56338c6a4..fd67884956 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -15,6 +15,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring.Legacy; using osu.Game.Users; using osu.Game.Utils; using Realms; @@ -63,6 +64,8 @@ namespace osu.Game.Scoring public double? PP { get; set; } + public int Version { get; set; } = LegacyScoreEncoder.LATEST_VERSION; + [Indexed] public long OnlineID { get; set; } = -1; From a9c65d200ae63be37d1a3663f9a9240d7d886397 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 26 Jun 2023 22:19:01 +0900 Subject: [PATCH 0394/2100] Initial conversion of scores --- .../Difficulty/CatchDifficultyCalculator.cs | 3 +- .../Difficulty/CatchScoreV1Processor.cs | 8 +- .../Difficulty/ManiaDifficultyCalculator.cs | 11 ++- .../Difficulty/ManiaScoreV1Processor.cs | 12 ++- .../Difficulty/OsuDifficultyCalculator.cs | 3 +- .../Difficulty/OsuScoreV1Processor.cs | 12 +-- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 + .../Difficulty/TaikoDifficultyCalculator.cs | 3 +- .../Difficulty/TaikoScoreV1Processor.cs | 18 ++-- osu.Game/BackgroundBeatmapProcessor.cs | 88 +++++++++++++++++++ osu.Game/Rulesets/Ruleset.cs | 2 + .../Rulesets/Scoring/ILegacyScoreProcessor.cs | 30 +++++++ osu.Game/Scoring/ScoreImporter.cs | 58 ++++++++++++ osu.Game/Scoring/ScoreManager.cs | 2 + 14 files changed, 225 insertions(+), 27 deletions(-) create mode 100644 osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index a44aaf6dfa..5e562237c8 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -51,7 +51,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty if (ComputeLegacyScoringValues) { - CatchScoreV1Processor sv1Processor = new CatchScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); + CatchScoreV1Processor sv1Processor = new CatchScoreV1Processor(); + sv1Processor.Simulate(workingBeatmap, beatmap, mods); attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; attributes.LegacyComboScore = sv1Processor.ComboScore; attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs index be48763845..bda6be66a4 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Difficulty { - internal class CatchScoreV1Processor + internal class CatchScoreV1Processor : ILegacyScoreProcessor { /// /// The accuracy portion of the legacy (ScoreV1) total score. @@ -36,10 +36,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty private int modernBonusScore; private int combo; - private readonly double scoreMultiplier; + private double scoreMultiplier; - public CatchScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) { + IBeatmap baseBeatmap = workingBeatmap.Beatmap; + int countNormal = 0; int countSlider = 0; int countSpinner = 0; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 4c419151c1..5403c1f860 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -31,9 +31,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty public override int Version => 20220902; + private IWorkingBeatmap workingBeatmap; + public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { + workingBeatmap = beatmap; + isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset); originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty; } @@ -58,8 +62,11 @@ namespace osu.Game.Rulesets.Mania.Difficulty if (ComputeLegacyScoringValues) { - ManiaScoreV1Processor sv1Processor = new ManiaScoreV1Processor(mods); - attributes.LegacyComboScore = sv1Processor.TotalScore; + ManiaScoreV1Processor sv1Processor = new ManiaScoreV1Processor(); + sv1Processor.Simulate(workingBeatmap, beatmap, mods); + attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; + attributes.LegacyComboScore = sv1Processor.ComboScore; + attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; } return attributes; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs index f28a86b6b4..9134ca4e2a 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs @@ -3,22 +3,26 @@ using System.Collections.Generic; using System.Linq; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Difficulty { - internal class ManiaScoreV1Processor + internal class ManiaScoreV1Processor : ILegacyScoreProcessor { - public int TotalScore { get; private set; } + public int AccuracyScore => 0; + public int ComboScore { get; private set; } + public double BonusScoreRatio => 0; - public ManiaScoreV1Processor(IReadOnlyList mods) + public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) { double multiplier = mods.Where(m => m is not (ModHidden or ModHardRock or ModDoubleTime or ModFlashlight or ManiaModFadeIn)) .Select(m => m.ScoreMultiplier) .Aggregate(1.0, (c, n) => c * n); - TotalScore = (int)(1000000 * multiplier); + ComboScore = (int)(1000000 * multiplier); } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 5158ea8a16..7ecbb48ae6 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -111,7 +111,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (ComputeLegacyScoringValues) { - OsuScoreV1Processor sv1Processor = new OsuScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); + OsuScoreV1Processor sv1Processor = new OsuScoreV1Processor(); + sv1Processor.Simulate(workingBeatmap, beatmap, mods); attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; attributes.LegacyComboScore = sv1Processor.ComboScore; attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs index e8231794e0..aa52edae87 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Difficulty { - internal class OsuScoreV1Processor + internal class OsuScoreV1Processor : ILegacyScoreProcessor { /// /// The accuracy portion of the legacy (ScoreV1) total score. @@ -36,18 +36,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty private int modernBonusScore; private int combo; - private readonly double scoreMultiplier; - private readonly IBeatmap playableBeatmap; + private double scoreMultiplier; + private IBeatmap playableBeatmap = null!; - public OsuScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) { this.playableBeatmap = playableBeatmap; + IBeatmap baseBeatmap = workingBeatmap.Beatmap; + int countNormal = 0; int countSlider = 0; int countSpinner = 0; - foreach (HitObject obj in baseBeatmap.HitObjects) + foreach (HitObject obj in workingBeatmap.Beatmap.HitObjects) { switch (obj) { diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 4cff16b46f..c82f10c017 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -322,5 +322,7 @@ namespace osu.Game.Rulesets.Osu } public override RulesetSetupSection CreateEditorSetupSection() => new OsuSetupSection(); + + public override ILegacyScoreProcessor CreateLegacyScoreProcessor() => new OsuScoreV1Processor(); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 1f34ba084f..b7f82b7512 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -101,7 +101,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (ComputeLegacyScoringValues) { - TaikoScoreV1Processor sv1Processor = new TaikoScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); + TaikoScoreV1Processor sv1Processor = new TaikoScoreV1Processor(); + sv1Processor.Simulate(workingBeatmap, beatmap, mods); attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; attributes.LegacyComboScore = sv1Processor.ComboScore; attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs index f01ca74f4a..255a3dd963 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty { - internal class TaikoScoreV1Processor + internal class TaikoScoreV1Processor : ILegacyScoreProcessor { /// /// The accuracy portion of the legacy (ScoreV1) total score. @@ -36,16 +36,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private int modernBonusScore; private int combo; - private readonly double modMultiplier; - private readonly int difficultyPeppyStars; - private readonly IBeatmap playableBeatmap; - private readonly IReadOnlyList mods; + private double modMultiplier; + private int difficultyPeppyStars; + private IBeatmap playableBeatmap = null!; + private IReadOnlyList mods = null!; - public TaikoScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) { this.playableBeatmap = playableBeatmap; this.mods = mods; + IBeatmap baseBeatmap = workingBeatmap.Beatmap; + int countNormal = 0; int countSlider = 0; int countSpinner = 0; @@ -205,10 +207,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (increaseCombo) combo++; - - if (hitObject is Swell) - { - } } } } diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index b8c89d8822..c49edec87d 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -14,6 +14,8 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -40,6 +42,9 @@ namespace osu.Game [Resolved] private ILocalUserPlayInfo? localUserPlayInfo { get; set; } + [Resolved] + private INotificationOverlay? notificationOverlay { get; set; } + protected virtual int TimeToSleepDuringGameplay => 30000; protected override void LoadComplete() @@ -52,6 +57,7 @@ namespace osu.Game checkForOutdatedStarRatings(); processBeatmapSetsWithMissingMetrics(); processScoresWithMissingStatistics(); + convertLegacyTotalScoreToStandardised(); }).ContinueWith(t => { if (t.Exception?.InnerException is ObjectDisposedException) @@ -193,5 +199,87 @@ namespace osu.Game } } } + + private void convertLegacyTotalScoreToStandardised() + { + HashSet scoreIds = new HashSet(); + + Logger.Log("Querying for scores that need total score conversion..."); + + realmAccess.Run(r => + { + foreach (var score in r.All().Where(s => s.IsLegacyScore)) + { + if (score.RulesetID is not (0 or 1 or 2 or 3)) + continue; + + if (score.Version >= 30000003) + continue; + + scoreIds.Add(score.ID); + } + }); + + Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); + + ProgressNotification? notification = null; + + if (scoreIds.Count > 0) + notificationOverlay?.Post(notification = new ProgressNotification { State = ProgressNotificationState.Active }); + + int count = 0; + updateNotification(); + + foreach (var id in scoreIds) + { + while (localUserPlayInfo?.IsPlaying.Value == true) + { + Logger.Log("Background processing sleeping due to active gameplay..."); + Thread.Sleep(TimeToSleepDuringGameplay); + } + + try + { + var score = scoreManager.Query(s => s.ID == id); + long newTotalScore = scoreManager.ConvertFromLegacyTotalScore(score); + + // Can't use async overload because we're not on the update thread. + // ReSharper disable once MethodHasAsyncOverload + realmAccess.Write(r => + { + ScoreInfo s = r.Find(id); + s.TotalScore = newTotalScore; + s.Version = 30000003; + }); + + Logger.Log($"Converted total score for score {id}"); + } + catch (Exception e) + { + Logger.Log($"Failed to convert total score for {id}: {e}"); + } + + ++count; + updateNotification(); + } + + void updateNotification() + { + if (notification == null) + return; + + if (count == scoreIds.Count) + { + notification.CompletionText = $"Total score updated for {scoreIds.Count} scores"; + notification.Progress = 1; + notification.State = ProgressNotificationState.Completed; + } + else + { + notification.Text = $"Total score updated for {count} of {scoreIds.Count} scores"; + notification.Progress = (float)count / scoreIds.Count; + } + } + } } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 490ec1475c..5501a3a7c5 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -380,5 +380,7 @@ namespace osu.Game.Rulesets /// Can be overridden to add a ruleset-specific section to the editor beatmap setup screen. /// public virtual RulesetSetupSection? CreateEditorSetupSection() => null; + + public virtual ILegacyScoreProcessor? CreateLegacyScoreProcessor() => null; } } diff --git a/osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs new file mode 100644 index 0000000000..70234a9b17 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs @@ -0,0 +1,30 @@ +// 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 osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Scoring +{ + public interface ILegacyScoreProcessor + { + /// + /// The accuracy portion of the legacy (ScoreV1) total score. + /// + int AccuracyScore { get; } + + /// + /// The combo-multiplied portion of the legacy (ScoreV1) total score. + /// + int ComboScore { get; } + + /// + /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. + /// This is made up of all judgements that would be or . + /// + double BonusScoreRatio { get; } + + void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods); + } +} diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 16658a598a..04a8bc6fc5 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -88,6 +88,8 @@ namespace osu.Game.Scoring // this requires: max combo, statistics, max statistics (where available), and mods to already be populated on the score. if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model)) model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model); + else if (model.IsLegacyScore) + model.TotalScore = ConvertFromLegacyTotalScore(model); } /// @@ -151,6 +153,62 @@ namespace osu.Game.Scoring #pragma warning restore CS0618 } + public long ConvertFromLegacyTotalScore(ScoreInfo score) + { + var beatmap = beatmaps().GetWorkingBeatmap(score.BeatmapInfo); + var ruleset = score.Ruleset.CreateInstance(); + + var sv1Processor = ruleset.CreateLegacyScoreProcessor(); + if (sv1Processor == null) + return score.TotalScore; + + sv1Processor.Simulate(beatmap, beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods), score.Mods); + + int maximumLegacyAccuracyScore = sv1Processor.AccuracyScore; + int maximumLegacyComboScore = sv1Processor.ComboScore; + double maximumLegacyBonusRatio = sv1Processor.BonusScoreRatio; + double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n); + + // The part of total score that doesn't include bonus. + int maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore; + + // The combo proportion is calculated as a proportion of maximumLegacyBaseScore. + double comboProportion = Math.Min(1, (double)score.TotalScore / maximumLegacyBaseScore); + + // The bonus proportion makes up the rest of the score that exceeds maximumLegacyBaseScore. + double bonusProportion = Math.Max(0, (score.TotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio); + + switch (ruleset.RulesetInfo.OnlineID) + { + case 0: + return (long)Math.Round(( + 700000 * comboProportion + + 300000 * Math.Pow(score.Accuracy, 10) + + bonusProportion) * modMultiplier); + + case 1: + return (long)Math.Round(( + 250000 * comboProportion + + 750000 * Math.Pow(score.Accuracy, 3.6) + + bonusProportion) * modMultiplier); + + case 2: + return (long)Math.Round(( + 600000 * comboProportion + + 400000 * score.Accuracy + + bonusProportion) * modMultiplier); + + case 3: + return (long)Math.Round(( + 990000 * comboProportion + + 10000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy) + + bonusProportion) * modMultiplier); + + default: + return score.TotalScore; + } + } + // Very naive local caching to improve performance of large score imports (where the username is usually the same for most or all scores). private readonly Dictionary usernameLookupCache = new Dictionary(); diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 55bcb9f79d..fd5e9c851c 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -169,6 +169,8 @@ namespace osu.Game.Scoring /// The score to populate the statistics of. public void PopulateMaximumStatistics(ScoreInfo score) => scoreImporter.PopulateMaximumStatistics(score); + public long ConvertFromLegacyTotalScore(ScoreInfo score) => scoreImporter.ConvertFromLegacyTotalScore(score); + #region Implementation of IPresentImports public Action>> PresentImport From cc45ec4fff329f4b0dda00d5cf347f6518e27165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 26 Jun 2023 18:52:05 +0200 Subject: [PATCH 0395/2100] Mark `IHasRecordingHandler.Recorder` nullable --- osu.Game/Rulesets/UI/IHasRecordingHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/IHasRecordingHandler.cs b/osu.Game/Rulesets/UI/IHasRecordingHandler.cs index 2a13272a98..f73398dd98 100644 --- a/osu.Game/Rulesets/UI/IHasRecordingHandler.cs +++ b/osu.Game/Rulesets/UI/IHasRecordingHandler.cs @@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.UI /// public interface IHasRecordingHandler { - public ReplayRecorder Recorder { set; } + public ReplayRecorder? Recorder { set; } } } From dbd76c1193bf545342e39cb67f997e6561224f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 26 Jun 2023 19:02:49 +0200 Subject: [PATCH 0396/2100] Fix attempting to add key counter controller to hierarchy in multiple places --- osu.Game/Screens/Play/HUDOverlay.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 9f3b7d5a93..b4f37f1df6 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -111,12 +111,14 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both; + // intentionally not added to hierarchy here as it will be attached via `BindDrawableRuleset()`. + KeyCounter = new KeyCounterController(); + Children = new[] { CreateFailingLayer(), //Needs to be initialized before skinnable drawables. tally = new JudgementTally(), - KeyCounter = new KeyCounterController(), mainComponents = new HUDComponentsContainer { AlwaysPresent = true, }, rulesetComponents = drawableRuleset != null ? new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, } From ff562e2dd7cb005a09d3443a7ac675f3739b90d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 26 Jun 2023 19:12:49 +0200 Subject: [PATCH 0397/2100] Remove redundant guard In this particular case guarding with `.IsNull()` makes no sense, as the `Trigger` is supplied in constructor and cannot feasibly be null. Doing that only makes sense in scenarios where BDL / dependency injection is involved. --- osu.Game/Screens/Play/HUD/KeyCounter.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/KeyCounter.cs b/osu.Game/Screens/Play/HUD/KeyCounter.cs index 08d7e79e7c..f12d2166fc 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounter.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounter.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; namespace osu.Game.Screens.Play.HUD @@ -49,11 +48,8 @@ namespace osu.Game.Screens.Play.HUD { base.Dispose(isDisposing); - if (Trigger.IsNotNull()) - { - Trigger.OnActivate -= Activate; - Trigger.OnDeactivate -= Deactivate; - } + Trigger.OnActivate -= Activate; + Trigger.OnDeactivate -= Deactivate; } } } From 4ac48e4cd86385c516e784580f146e593c8486aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 26 Jun 2023 19:15:05 +0200 Subject: [PATCH 0398/2100] Group input handling members together --- osu.Game/Screens/Play/HUD/KeyCounterController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/KeyCounterController.cs b/osu.Game/Screens/Play/HUD/KeyCounterController.cs index 93e0528e6b..e4d5968f26 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterController.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterController.cs @@ -36,8 +36,8 @@ namespace osu.Game.Screens.Play.HUD } public void AddRange(IEnumerable inputTriggers) => inputTriggers.ForEach(Add); - public override bool HandleNonPositionalInput => true; + public override bool HandleNonPositionalInput => true; public override bool HandlePositionalInput => true; } } From 8d91580dc19f34f5dc782df36236ca69e33d955e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 26 Jun 2023 19:27:42 +0200 Subject: [PATCH 0399/2100] Rename `{KeyCounter -> InputCount}Controller` --- osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs | 6 +++--- .../Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs | 2 +- .../Visual/Gameplay/TestSceneGameplayRewinding.cs | 4 ++-- osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs | 4 ++-- osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs | 2 +- .../Gameplay/TestSceneSkinEditorMultipleSkins.cs | 2 +- .../Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs | 2 +- osu.Game/Rulesets/UI/DrawableRuleset.cs | 4 ++-- osu.Game/Rulesets/UI/ICanAttachHUDPieces.cs | 2 +- osu.Game/Rulesets/UI/RulesetInputManager.cs | 6 +++--- ...KeyCounterController.cs => InputCountController.cs} | 4 ++-- osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs | 2 +- osu.Game/Screens/Play/HUDOverlay.cs | 10 +++++----- osu.Game/Screens/Play/Player.cs | 4 ++-- 15 files changed, 28 insertions(+), 28 deletions(-) rename osu.Game/Screens/Play/HUD/{KeyCounterController.cs => InputCountController.cs} (92%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index c829b73f66..3ac4d25028 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -35,14 +35,14 @@ namespace osu.Game.Tests.Visual.Gameplay var referenceBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); - AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Triggers.Any(kc => kc.ActivationCount.Value > 2)); + AddUntilStep("key counter counted keys", () => Player.HUDOverlay.InputCountController.Triggers.Any(kc => kc.ActivationCount.Value > 2)); seekTo(referenceBeatmap.Breaks[0].StartTime); - AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting.Value); + AddAssert("keys not counting", () => !Player.HUDOverlay.InputCountController.IsCounting.Value); AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType().Single().AccuracyDisplay.Current.Value == 1); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); - AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Triggers.All(kc => kc.ActivationCount.Value == 0)); + AddUntilStep("key counter reset", () => Player.HUDOverlay.InputCountController.Triggers.All(kc => kc.ActivationCount.Value == 0)); seekTo(referenceBeatmap.HitObjects[^1].GetEndTime()); AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index 56c405d81f..18a180589b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Gameplay (typeof(HealthProcessor), actualComponentsContainer.Dependencies.Get()), (typeof(GameplayState), actualComponentsContainer.Dependencies.Get()), (typeof(IGameplayClock), actualComponentsContainer.Dependencies.Get()), - (typeof(KeyCounterController), actualComponentsContainer.Dependencies.Get()) + (typeof(InputCountController), actualComponentsContainer.Dependencies.Get()) }, }; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs index 508cf192d3..0cb74ecde6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs @@ -31,11 +31,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); addSeekStep(3000); AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged)); - AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Triggers.Select(kc => kc.ActivationCount.Value).Sum() == 15); + AddUntilStep("key counter counted keys", () => Player.HUDOverlay.InputCountController.Triggers.Select(kc => kc.ActivationCount.Value).Sum() == 15); AddStep("clear results", () => Player.Results.Clear()); addSeekStep(0); AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged)); - AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Triggers.All(kc => kc.ActivationCount.Value == 0)); + AddUntilStep("key counters reset", () => Player.HUDOverlay.InputCountController.Triggers.All(kc => kc.ActivationCount.Value == 0)); AddAssert("no results triggered", () => Player.Results.Count == 0); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 8cde2ab81f..7552c059a2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -269,7 +269,7 @@ namespace osu.Game.Tests.Visual.Gameplay hudOverlay = new HUDOverlay(null, Array.Empty()); // Add any key just to display the key counter visually. - hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); + hudOverlay.InputCountController.Add(new KeyCounterKeyboardTrigger(Key.Space)); scoreProcessor.Combo.Value = 1; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 786dcc873a..a2c227a76a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -18,13 +18,13 @@ namespace osu.Game.Tests.Visual.Gameplay public partial class TestSceneKeyCounter : OsuManualInputManagerTestScene { [Cached] - private readonly KeyCounterController controller; + private readonly InputCountController controller; public TestSceneKeyCounter() { Children = new Drawable[] { - controller = new KeyCounterController(), + controller = new InputCountController(), new FillFlowContainer { Anchor = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs index 5fb9bf004f..94a25d064c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { AddUntilStep("score above zero", () => ((ScoreAccessibleReplayPlayer)Player).ScoreProcessor.TotalScore.Value > 0); - AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Triggers.Any(kc => kc.ActivationCount.Value > 0)); + AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.InputCountController.Triggers.Any(kc => kc.ActivationCount.Value > 0)); AddAssert("cannot fail", () => !((ScoreAccessibleReplayPlayer)Player).AllowFail); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index 4ae115a68d..89eb291ab0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Gameplay }; // Add any key just to display the key counter visually. - hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); + hudOverlay.InputCountController.Add(new KeyCounterKeyboardTrigger(Key.Space)); scoreProcessor.Combo.Value = 1; return new Container diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index cab52ddab5..be73e36e11 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.Gameplay hudOverlay = new HUDOverlay(null, Array.Empty()); // Add any key just to display the key counter visually. - hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); + hudOverlay.InputCountController.Add(new KeyCounterKeyboardTrigger(Key.Space)); action?.Invoke(hudOverlay); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 3e390a8931..326c77e94c 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -335,8 +335,8 @@ namespace osu.Game.Rulesets.UI /// The representing . public abstract DrawableHitObject CreateDrawableRepresentation(TObject h); - public void Attach(KeyCounterController keyCounter) => - (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(keyCounter); + public void Attach(InputCountController inputCountController) => + (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(inputCountController); public void Attach(ClicksPerSecondCalculator calculator) => (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(calculator); diff --git a/osu.Game/Rulesets/UI/ICanAttachHUDPieces.cs b/osu.Game/Rulesets/UI/ICanAttachHUDPieces.cs index b81af8556e..1f93d25720 100644 --- a/osu.Game/Rulesets/UI/ICanAttachHUDPieces.cs +++ b/osu.Game/Rulesets/UI/ICanAttachHUDPieces.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.UI /// public interface ICanAttachHUDPieces { - void Attach(KeyCounterController keyCounter); + void Attach(InputCountController inputCountController); void Attach(ClicksPerSecondCalculator calculator); } } diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 9f4fabc300..2ec3985d36 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -160,11 +160,11 @@ namespace osu.Game.Rulesets.UI #region Key Counter Attachment - public void Attach(KeyCounterController keyCounter) + public void Attach(InputCountController inputCountController) { - KeyBindingContainer.Add(keyCounter); + KeyBindingContainer.Add(inputCountController); - keyCounter.AddRange(KeyBindingContainer.DefaultKeyBindings + inputCountController.AddRange(KeyBindingContainer.DefaultKeyBindings .Select(b => b.GetAction()) .Distinct() .OrderBy(action => action) diff --git a/osu.Game/Screens/Play/HUD/KeyCounterController.cs b/osu.Game/Screens/Play/HUD/InputCountController.cs similarity index 92% rename from osu.Game/Screens/Play/HUD/KeyCounterController.cs rename to osu.Game/Screens/Play/HUD/InputCountController.cs index e4d5968f26..47163eeb6d 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterController.cs +++ b/osu.Game/Screens/Play/HUD/InputCountController.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.Play.HUD /// Keeps track of key press counts for a current play session, exposing bindable counts which can /// be used for display purposes. /// - public partial class KeyCounterController : CompositeComponent + public partial class InputCountController : CompositeComponent { public readonly Bindable IsCounting = new BindableBool(true); @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Play.HUD public IReadOnlyList Triggers => triggers; - public KeyCounterController() + public InputCountController() { InternalChild = triggers = new Container(); } diff --git a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs index efe51d75b0..e222099c63 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Play.HUD protected readonly Bindable ConfigVisibility = new Bindable(); [Resolved] - private KeyCounterController controller { get; set; } = null!; + private InputCountController controller { get; set; } = null!; protected abstract void UpdateVisibility(); diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index b4f37f1df6..dfc12c06d8 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -62,7 +62,7 @@ namespace osu.Game.Screens.Play private readonly ClicksPerSecondCalculator clicksPerSecondCalculator; [Cached] - public readonly KeyCounterController KeyCounter; + public readonly InputCountController InputCountController; [Cached] private readonly JudgementTally tally; @@ -112,7 +112,7 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both; // intentionally not added to hierarchy here as it will be attached via `BindDrawableRuleset()`. - KeyCounter = new KeyCounterController(); + InputCountController = new InputCountController(); Children = new[] { @@ -307,13 +307,13 @@ namespace osu.Game.Screens.Play { PlayerSettingsOverlay.Show(); ModDisplay.FadeIn(200); - KeyCounter.Margin = new MarginPadding(10) { Bottom = 30 }; + InputCountController.Margin = new MarginPadding(10) { Bottom = 30 }; } else { PlayerSettingsOverlay.Hide(); ModDisplay.Delay(2000).FadeOut(200); - KeyCounter.Margin = new MarginPadding(10); + InputCountController.Margin = new MarginPadding(10); } updateVisibility(); @@ -323,7 +323,7 @@ namespace osu.Game.Screens.Play { if (drawableRuleset is ICanAttachHUDPieces attachTarget) { - attachTarget.Attach(KeyCounter); + attachTarget.Attach(InputCountController); attachTarget.Attach(clicksPerSecondCalculator); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9fc97162bf..1c6d1fc6a5 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -432,7 +432,7 @@ namespace osu.Game.Screens.Play IsPaused = { BindTarget = GameplayClockContainer.IsPaused }, ReplayLoaded = { BindTarget = DrawableRuleset.HasReplayLoaded }, }, - KeyCounter = + InputCountController = { IsCounting = { @@ -477,7 +477,7 @@ namespace osu.Game.Screens.Play { updateGameplayState(); updatePauseOnFocusLostState(); - HUDOverlay.KeyCounter.IsCounting.Value = !isBreakTime.NewValue; + HUDOverlay.InputCountController.IsCounting.Value = !isBreakTime.NewValue; } private void updateGameplayState() From 7200855d46c4398aeca84ea2bca45fccc38736a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 26 Jun 2023 19:30:04 +0200 Subject: [PATCH 0400/2100] Rename `Judgement{Tally -> CountController}` --- osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs | 6 +++--- .../{JudgementTally.cs => JudgementCountController.cs} | 2 +- .../Screens/Play/HUD/JudgementCounter/JudgementCounter.cs | 4 ++-- .../Play/HUD/JudgementCounter/JudgementCounterDisplay.cs | 6 +++--- osu.Game/Screens/Play/HUDOverlay.cs | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) rename osu.Game/Screens/Play/HUD/JudgementCounter/{JudgementTally.cs => JudgementCountController.cs} (96%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs index 5a802e0d36..f117569657 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Gameplay public partial class TestSceneJudgementCounter : OsuTestScene { private ScoreProcessor scoreProcessor = null!; - private JudgementTally judgementTally = null!; + private JudgementCountController judgementCountController = null!; private TestJudgementCounterDisplay counterDisplay = null!; private DependencyProvidingContainer content = null!; @@ -47,11 +47,11 @@ namespace osu.Game.Tests.Visual.Gameplay CachedDependencies = new (Type, object)[] { (typeof(ScoreProcessor), scoreProcessor), (typeof(Ruleset), ruleset) }, Children = new Drawable[] { - judgementTally = new JudgementTally(), + judgementCountController = new JudgementCountController(), content = new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, - CachedDependencies = new (Type, object)[] { (typeof(JudgementTally), judgementTally) }, + CachedDependencies = new (Type, object)[] { (typeof(JudgementCountController), judgementCountController) }, } }, }; diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementTally.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs similarity index 96% rename from osu.Game/Screens/Play/HUD/JudgementCounter/JudgementTally.cs rename to osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs index e9e3fde92a..98e74a0e7e 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementTally.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter /// Keeps track of judgements for a current play session, exposing bindable counts which can /// be used for display purposes. /// - public partial class JudgementTally : CompositeDrawable + public partial class JudgementCountController : CompositeDrawable { [Resolved] private ScoreProcessor scoreProcessor { get; set; } = null!; diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs index 7675d0cc4f..6c417faac2 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs @@ -18,9 +18,9 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter public BindableBool ShowName = new BindableBool(); public Bindable Direction = new Bindable(); - public readonly JudgementTally.JudgementCount Result; + public readonly JudgementCountController.JudgementCount Result; - public JudgementCounter(JudgementTally.JudgementCount result) => Result = result; + public JudgementCounter(JudgementCountController.JudgementCount result) => Result = result; public OsuSpriteText ResultName = null!; private FillFlowContainer flowContainer = null!; diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs index a9b59a02b5..1dbee19ee3 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter public BindableBool ShowMaxJudgement { get; set; } = new BindableBool(true); [Resolved] - private JudgementTally tally { get; set; } = null!; + private JudgementCountController judgementCountController { get; set; } = null!; protected FillFlowContainer CounterFlow = null!; @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter AutoSizeAxes = Axes.Both }; - foreach (var result in tally.Results) + foreach (var result in judgementCountController.Results) CounterFlow.Add(createCounter(result)); } @@ -123,7 +123,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter } } - private JudgementCounter createCounter(JudgementTally.JudgementCount info) => + private JudgementCounter createCounter(JudgementCountController.JudgementCount info) => new JudgementCounter(info) { State = { Value = Visibility.Hidden }, diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index dfc12c06d8..ae9c6a7d87 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -65,7 +65,7 @@ namespace osu.Game.Screens.Play public readonly InputCountController InputCountController; [Cached] - private readonly JudgementTally tally; + private readonly JudgementCountController judgementCountController; public Bindable ShowHealthBar = new Bindable(true); @@ -118,7 +118,7 @@ namespace osu.Game.Screens.Play { CreateFailingLayer(), //Needs to be initialized before skinnable drawables. - tally = new JudgementTally(), + judgementCountController = new JudgementCountController(), mainComponents = new HUDComponentsContainer { AlwaysPresent = true, }, rulesetComponents = drawableRuleset != null ? new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, } From e1cbcabe0b4c68625cc4010f2e26cec228f95d85 Mon Sep 17 00:00:00 2001 From: timiimit Date: Mon, 22 May 2023 17:00:53 +0200 Subject: [PATCH 0401/2100] Fix skip not always triggering in multiplayer --- osu.Game/Screens/Play/Player.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 18ea9d0acb..9eec60f23a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -378,9 +378,6 @@ namespace osu.Game.Screens.Play IsBreakTime.BindTo(breakTracker.IsBreakTime); IsBreakTime.BindValueChanged(onBreakTimeChanged, true); - if (Configuration.AutomaticallySkipIntro) - skipIntroOverlay.SkipWhenReady(); - loadLeaderboard(); } @@ -1087,6 +1084,9 @@ namespace osu.Game.Screens.Play throw new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running"); GameplayClockContainer.Reset(startClock: true); + + if (Configuration.AutomaticallySkipIntro) + skipIntroOverlay.SkipWhenReady(); } public override void OnSuspending(ScreenTransitionEvent e) From 8a7a42b7ec28fc7af008783cff236b3dbde67a98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 26 Jun 2023 22:19:52 +0200 Subject: [PATCH 0402/2100] Remove weird nullable enable and double licence header --- osu.Game/Screens/Select/FilterCriteria.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 3fcc8d5476..0024c431f1 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -1,10 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable enable -// 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; From 4873aaf7ede1d7558cc551d0744dbec3637543b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 26 Jun 2023 22:27:08 +0200 Subject: [PATCH 0403/2100] Add failing test case for filters potentially crashing due to invalid regex --- osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index 8bd9407599..d8a56ea478 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -169,6 +169,7 @@ namespace osu.Game.Tests.NonVisual.Filtering [TestCase("\"tags to\"", true)] [TestCase("\"version\"", false)] [TestCase("\"an auteur\"", true)] + [TestCase("\"\\\"", true)] // nasty case, covers properly escaping user input in underlying regex. public void TestCriteriaMatchingExactTerms(string terms, bool filtered) { var exampleBeatmapInfo = getExampleBeatmap(); From 4cb122dad46b3cff9de615b47e7220fd8488297d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 26 Jun 2023 22:27:48 +0200 Subject: [PATCH 0404/2100] Escape user input before embedding into regex --- osu.Game/Screens/Select/FilterCriteria.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 0024c431f1..22780acfc3 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -150,7 +150,7 @@ namespace osu.Game.Screens.Select return false; if (Exact) - return Regex.IsMatch(value, $@"(^|\s){searchTerm}($|\s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + return Regex.IsMatch(value, $@"(^|\s){Regex.Escape(searchTerm)}($|\s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase); } From e998be0eee006224e3212ed800901aa6237ecce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 26 Jun 2023 22:29:20 +0200 Subject: [PATCH 0405/2100] Use `== true` rather than `?? false` --- osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index f020cd02f7..00a0d4a849 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -210,7 +210,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Click(MouseButton.Left); }); - AddAssert("collection filter still selected", () => control.CreateCriteria().CollectionBeatmapMD5Hashes?.Any() ?? false); + AddAssert("collection filter still selected", () => control.CreateCriteria().CollectionBeatmapMD5Hashes?.Any() == true); AddAssert("filter request not fired", () => !received); } From 40ceb4dfac1f978cd0e6bb33a20f9e61dcc0d40e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 26 Jun 2023 22:40:25 +0200 Subject: [PATCH 0406/2100] Fix incorrect indent size --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 2ec3985d36..08e180536f 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -165,10 +165,10 @@ namespace osu.Game.Rulesets.UI KeyBindingContainer.Add(inputCountController); inputCountController.AddRange(KeyBindingContainer.DefaultKeyBindings - .Select(b => b.GetAction()) - .Distinct() - .OrderBy(action => action) - .Select(action => new KeyCounterActionTrigger(action))); + .Select(b => b.GetAction()) + .Distinct() + .OrderBy(action => action) + .Select(action => new KeyCounterActionTrigger(action))); } #endregion From 9c87d42f2be938162544f6a3a6ac7dd0b97e605b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 26 Jun 2023 22:41:31 +0200 Subject: [PATCH 0407/2100] Attempt to remedy HUD overlay test failure by waiting more --- osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 7552c059a2..2d1af1386c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -145,11 +145,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); - AddAssert("key counters hidden", () => !keyCounterFlow.IsPresent); + AddUntilStep("key counters hidden", () => !keyCounterFlow.IsPresent); AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true); AddUntilStep("hidetarget is visible", () => hideTarget.Alpha, () => Is.GreaterThan(0)); - AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent); + AddUntilStep("key counters still hidden", () => !keyCounterFlow.IsPresent); } [Test] From 0c5c09597c382e7f6da25bb5c61d4972cafbf0ff Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 27 Jun 2023 14:59:40 +0900 Subject: [PATCH 0408/2100] Store old total score as LegacyTotalScore --- osu.Game/Database/RealmAccess.cs | 5 ++++- osu.Game/Scoring/ScoreImporter.cs | 7 +++++-- osu.Game/Scoring/ScoreInfo.cs | 2 ++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index e3423d25c5..727ddf06d7 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -78,7 +78,7 @@ namespace osu.Game.Database /// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files. /// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes. /// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations. - /// 31 2023-06-26 Add Version to ScoreInfo, set to 30000002. + /// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and move TotalScore into LegacyTotalScore for legacy scores. /// private const int schema_version = 31; @@ -973,7 +973,10 @@ namespace osu.Game.Database var scores = migration.NewRealm.All(); foreach (var score in scores) + { + score.LegacyTotalScore = score.TotalScore; score.Version = 30000002; // Last version before legacy total score conversion. + } break; } diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 04a8bc6fc5..e8f23fdc10 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -155,6 +155,9 @@ namespace osu.Game.Scoring public long ConvertFromLegacyTotalScore(ScoreInfo score) { + if (!score.IsLegacyScore) + return score.TotalScore; + var beatmap = beatmaps().GetWorkingBeatmap(score.BeatmapInfo); var ruleset = score.Ruleset.CreateInstance(); @@ -173,10 +176,10 @@ namespace osu.Game.Scoring int maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore; // The combo proportion is calculated as a proportion of maximumLegacyBaseScore. - double comboProportion = Math.Min(1, (double)score.TotalScore / maximumLegacyBaseScore); + double comboProportion = Math.Min(1, (double)score.LegacyTotalScore / maximumLegacyBaseScore); // The bonus proportion makes up the rest of the score that exceeds maximumLegacyBaseScore. - double bonusProportion = Math.Max(0, (score.TotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio); + double bonusProportion = Math.Max(0, (score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio); switch (ruleset.RulesetInfo.OnlineID) { diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index fd67884956..5de1c69d8a 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -54,6 +54,8 @@ namespace osu.Game.Scoring public long TotalScore { get; set; } + public long LegacyTotalScore { get; set; } + public int MaxCombo { get; set; } public double Accuracy { get; set; } From 702266198b011734a5b679f08d079d4e352331d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Jun 2023 15:21:13 +0900 Subject: [PATCH 0409/2100] Add missing "title=" search support at song select --- .../NonVisual/Filtering/FilterMatchingTest.cs | 23 ++++++++++++++++++- .../Select/Carousel/CarouselBeatmap.cs | 2 ++ osu.Game/Screens/Select/FilterCriteria.cs | 1 + osu.Game/Screens/Select/FilterQueryParser.cs | 3 +++ 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index d8a56ea478..fe5511add1 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.NonVisual.Filtering Artist = "The Artist", ArtistUnicode = "check unicode too", Title = "Title goes here", - TitleUnicode = "Title goes here", + TitleUnicode = "TitleUnicode goes here", Author = { Username = "The Author" }, Source = "unit tests", Tags = "look for tags too", @@ -204,6 +204,27 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual(filtered, carouselItem.Filtered.Value); } + [Test] + [TestCase("", false)] + [TestCase("Goes", false)] + [TestCase("GOES", false)] + [TestCase("goes", false)] + [TestCase("title goes", false)] + [TestCase("title goes AND then something else", true)] + [TestCase("titleunicode", false)] + [TestCase("unknown", true)] + public void TestCriteriaMatchingTitle(string titleName, bool filtered) + { + var exampleBeatmapInfo = getExampleBeatmap(); + var criteria = new FilterCriteria + { + Title = new FilterCriteria.OptionalTextFilter { SearchTerm = titleName } + }; + var carouselItem = new CarouselBeatmap(exampleBeatmapInfo); + carouselItem.Filter(criteria); + Assert.AreEqual(filtered, carouselItem.Filtered.Value); + } + [Test] [TestCase("", false)] [TestCase("The", false)] diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 18931c462f..6917bd1da2 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -57,6 +57,8 @@ namespace osu.Game.Screens.Select.Carousel match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(BeatmapInfo.Metadata.Author.Username); match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) || criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode); + match &= !criteria.Title.HasFilter || criteria.Title.Matches(BeatmapInfo.Metadata.Title) || + criteria.Title.Matches(BeatmapInfo.Metadata.TitleUnicode); match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating); diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 22780acfc3..680da21dbc 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -31,6 +31,7 @@ namespace osu.Game.Screens.Select public OptionalRange OnlineStatus; public OptionalTextFilter Creator; public OptionalTextFilter Artist; + public OptionalTextFilter Title; public OptionalRange UserStarDifficulty = new OptionalRange { diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 474a9fdfea..20a5d5d1a6 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -73,6 +73,9 @@ namespace osu.Game.Screens.Select case "artist": return TryUpdateCriteriaText(ref criteria.Artist, op, value); + case "title": + return TryUpdateCriteriaText(ref criteria.Title, op, value); + default: return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false; } From c423f77d535b33a07d5930ea73499d0e621069f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Jun 2023 15:34:33 +0900 Subject: [PATCH 0410/2100] Add support for matching full terms using suffixed `!` --- osu.Game/Screens/Select/FilterCriteria.cs | 53 +++++++++++++++++--- osu.Game/Screens/Select/FilterQueryParser.cs | 2 +- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 22780acfc3..c8804528c7 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using osu.Game.Beatmaps; @@ -62,7 +63,7 @@ namespace osu.Game.Screens.Select string remainingText = value; // First handle quoted segments to ensure we keep inline spaces in exact matches. - foreach (Match quotedSegment in Regex.Matches(searchText, "(\"[^\"]+\")")) + foreach (Match quotedSegment in Regex.Matches(searchText, "(\"[^\"]+\"[!]?)")) { terms.Add(new OptionalTextFilter { SearchTerm = quotedSegment.Value }); remainingText = remainingText.Replace(quotedSegment.Value, string.Empty); @@ -138,7 +139,7 @@ namespace osu.Game.Screens.Select { public bool HasFilter => !string.IsNullOrEmpty(SearchTerm); - public bool Exact { get; private set; } + public MatchMode MatchMode { get; private set; } public bool Matches(string value) { @@ -149,10 +150,18 @@ namespace osu.Game.Screens.Select if (string.IsNullOrEmpty(value)) return false; - if (Exact) - return Regex.IsMatch(value, $@"(^|\s){Regex.Escape(searchTerm)}($|\s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + switch (MatchMode) + { + default: + case MatchMode.None: + return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase); - return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase); + case MatchMode.IsolatedPhrase: + return Regex.IsMatch(value, $@"(^|\s){Regex.Escape(searchTerm)}($|\s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + case MatchMode.FullPhrase: + return CultureInfo.InvariantCulture.CompareInfo.Compare(value, searchTerm, CompareOptions.IgnoreCase) == 0; + } } private string searchTerm; @@ -162,12 +171,42 @@ namespace osu.Game.Screens.Select get => searchTerm; set { - searchTerm = value.Trim('"'); - Exact = searchTerm != value; + searchTerm = value; + + if (searchTerm.EndsWith("\"!", StringComparison.Ordinal)) + { + searchTerm = searchTerm.Trim('!', '\"'); + MatchMode = MatchMode.FullPhrase; + } + else if (searchTerm.StartsWith('\"')) + { + searchTerm = searchTerm.Trim('\"'); + MatchMode = MatchMode.IsolatedPhrase; + } + else + MatchMode = MatchMode.None; } } public bool Equals(OptionalTextFilter other) => SearchTerm == other.SearchTerm; } + + public enum MatchMode + { + /// + /// Match using a simple "contains" substring match. + /// + None, + + /// + /// Match for the search phrase being isolated by spaces, or at the start or end of the text. + /// + IsolatedPhrase, + + /// + /// Match for the search phrase matching the full text in completion. + /// + FullPhrase, + } } } diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 474a9fdfea..4bc4448291 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Select public static class FilterQueryParser { private static readonly Regex query_syntax_regex = new Regex( - @"\b(?\w+)(?(:|=|(>|<)(:|=)?))(?("".*"")|(\S*))", + @"\b(?\w+)(?(:|=|(>|<)(:|=)?))(?("".*""[!]?)|(\S*))", RegexOptions.Compiled | RegexOptions.IgnoreCase); internal static void ApplyQueries(FilterCriteria criteria, string query) From 19ec6d5455bdd68c01fee93ccce24b6bc5796640 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Jun 2023 15:44:58 +0900 Subject: [PATCH 0411/2100] Add test coverage of new matching mode --- .../NonVisual/Filtering/FilterMatchingTest.cs | 4 +++ .../Filtering/FilterQueryParserTest.cs | 30 +++++++++++++------ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index d8a56ea478..89c3009950 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -169,6 +169,8 @@ namespace osu.Game.Tests.NonVisual.Filtering [TestCase("\"tags to\"", true)] [TestCase("\"version\"", false)] [TestCase("\"an auteur\"", true)] + [TestCase("\"Artist\"!", true)] + [TestCase("\"The Artist\"!", false)] [TestCase("\"\\\"", true)] // nasty case, covers properly escaping user input in underlying regex. public void TestCriteriaMatchingExactTerms(string terms, bool filtered) { @@ -213,6 +215,8 @@ namespace osu.Game.Tests.NonVisual.Filtering [TestCase("the artist AND then something else", true)] [TestCase("unicode too", false)] [TestCase("unknown", true)] + [TestCase("\"Artist\"!", true)] + [TestCase("\"The Artist\"!", false)] public void TestCriteriaMatchingArtist(string artistName, bool filtered) { var exampleBeatmapInfo = getExampleBeatmap(); diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index 46f91f9a67..7bf23f1a2e 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -26,26 +26,26 @@ namespace osu.Game.Tests.NonVisual.Filtering [Test] public void TestApplyQueriesBareWordsWithExactMatch() { - const string query = "looking for \"a beatmap\" like \"this\""; + const string query = "looking for \"a beatmap\"! like \"this\""; var filterCriteria = new FilterCriteria(); FilterQueryParser.ApplyQueries(filterCriteria, query); - Assert.AreEqual("looking for \"a beatmap\" like \"this\"", filterCriteria.SearchText); + Assert.AreEqual("looking for \"a beatmap\"! like \"this\"", filterCriteria.SearchText); Assert.AreEqual(5, filterCriteria.SearchTerms.Length); Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("a beatmap")); - Assert.That(filterCriteria.SearchTerms[0].Exact, Is.True); + Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.FullPhrase)); Assert.That(filterCriteria.SearchTerms[1].SearchTerm, Is.EqualTo("this")); - Assert.That(filterCriteria.SearchTerms[1].Exact, Is.True); + Assert.That(filterCriteria.SearchTerms[1].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase)); Assert.That(filterCriteria.SearchTerms[2].SearchTerm, Is.EqualTo("looking")); - Assert.That(filterCriteria.SearchTerms[2].Exact, Is.False); + Assert.That(filterCriteria.SearchTerms[2].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.None)); Assert.That(filterCriteria.SearchTerms[3].SearchTerm, Is.EqualTo("for")); - Assert.That(filterCriteria.SearchTerms[3].Exact, Is.False); + Assert.That(filterCriteria.SearchTerms[3].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.None)); Assert.That(filterCriteria.SearchTerms[4].SearchTerm, Is.EqualTo("like")); - Assert.That(filterCriteria.SearchTerms[4].Exact, Is.False); + Assert.That(filterCriteria.SearchTerms[4].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.None)); } /* @@ -260,7 +260,7 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual("find me songs by please", filterCriteria.SearchText.Trim()); Assert.AreEqual(5, filterCriteria.SearchTerms.Length); Assert.AreEqual("singer", filterCriteria.Artist.SearchTerm); - Assert.That(filterCriteria.Artist.Exact, Is.False); + Assert.That(filterCriteria.Artist.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.None)); } [Test] @@ -272,7 +272,19 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual("really like yes", filterCriteria.SearchText.Trim()); Assert.AreEqual(3, filterCriteria.SearchTerms.Length); Assert.AreEqual("name with space", filterCriteria.Artist.SearchTerm); - Assert.That(filterCriteria.Artist.Exact, Is.True); + Assert.That(filterCriteria.Artist.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase)); + } + + [Test] + public void TestApplyArtistQueriesWithSpacesFullPhrase() + { + const string query = "artist=\"The Only One\"!"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.That(filterCriteria.SearchText.Trim(), Is.Empty); + Assert.AreEqual(0, filterCriteria.SearchTerms.Length); + Assert.AreEqual("The Only One", filterCriteria.Artist.SearchTerm); + Assert.That(filterCriteria.Artist.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.FullPhrase)); } [Test] From e99de0eb5db05f2caf841b96672e664fea947d53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Jun 2023 16:04:34 +0900 Subject: [PATCH 0412/2100] Add safety to tests to ensure loaded --- osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 2d1af1386c..74249007e4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -236,7 +236,6 @@ namespace osu.Game.Tests.Visual.Gameplay createNew(); - AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded); AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType().Single().Alpha == 0); AddUntilStep("wait for hud load", () => hudOverlay.ChildrenOfType().All(c => c.ComponentsLoaded)); @@ -255,7 +254,6 @@ namespace osu.Game.Tests.Visual.Gameplay createNew(); - AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded); AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType().Single().Alpha == 0); AddStep("reload components", () => hudOverlay.ChildrenOfType().Single().Reload()); @@ -277,6 +275,9 @@ namespace osu.Game.Tests.Visual.Gameplay Child = hudOverlay; }); + + AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded); + AddUntilStep("wait for components present", () => hudOverlay.ChildrenOfType().FirstOrDefault() != null); } protected override void Dispose(bool isDisposing) From e21583ff1b642e404bd604d4dd045fd7d23cc2ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Jun 2023 16:35:59 +0900 Subject: [PATCH 0413/2100] Refactor `InputCountController` to not require being added to foreign body via `Attach` I've made the flow match `ClicksPerSecondCalculator` as close as possible. Hopefully this reads better. --- .../Visual/Gameplay/TestSceneKeyCounter.cs | 7 +++- osu.Game/Rulesets/UI/RulesetInputManager.cs | 21 +++++----- .../Screens/Play/HUD/InputCountController.cs | 17 +++----- .../Screens/Play/HUD/KeyCounterDisplay.cs | 39 +++++-------------- osu.Game/Screens/Play/HUDOverlay.cs | 4 +- 5 files changed, 29 insertions(+), 59 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index a2c227a76a..3df0f18558 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -48,13 +48,16 @@ namespace osu.Game.Tests.Visual.Gameplay } }; - controller.AddRange(new InputTrigger[] + var inputTriggers = new InputTrigger[] { new KeyCounterKeyboardTrigger(Key.X), new KeyCounterKeyboardTrigger(Key.X), new KeyCounterMouseTrigger(MouseButton.Left), new KeyCounterMouseTrigger(MouseButton.Right), - }); + }; + + AddRange(inputTriggers); + controller.AddRange(inputTriggers); AddStep("Add random", () => { diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 08e180536f..9be210f4b2 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -162,25 +162,22 @@ namespace osu.Game.Rulesets.UI public void Attach(InputCountController inputCountController) { - KeyBindingContainer.Add(inputCountController); + var triggers = KeyBindingContainer.DefaultKeyBindings + .Select(b => b.GetAction()) + .Distinct() + .OrderBy(action => action) + .Select(action => new KeyCounterActionTrigger(action)) + .ToArray(); - inputCountController.AddRange(KeyBindingContainer.DefaultKeyBindings - .Select(b => b.GetAction()) - .Distinct() - .OrderBy(action => action) - .Select(action => new KeyCounterActionTrigger(action))); + KeyBindingContainer.AddRange(triggers); + inputCountController.AddRange(triggers); } #endregion #region Keys per second Counter Attachment - public void Attach(ClicksPerSecondCalculator calculator) - { - var listener = new ActionListener(calculator); - - KeyBindingContainer.Add(listener); - } + public void Attach(ClicksPerSecondCalculator calculator) => KeyBindingContainer.Add(new ActionListener(calculator)); private partial class ActionListener : Component, IKeyBindingHandler { diff --git a/osu.Game/Screens/Play/HUD/InputCountController.cs b/osu.Game/Screens/Play/HUD/InputCountController.cs index 47163eeb6d..4827f2315f 100644 --- a/osu.Game/Screens/Play/HUD/InputCountController.cs +++ b/osu.Game/Screens/Play/HUD/InputCountController.cs @@ -1,7 +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.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; @@ -17,26 +16,20 @@ namespace osu.Game.Screens.Play.HUD { public readonly Bindable IsCounting = new BindableBool(true); - public event Action? OnNewTrigger; + private readonly BindableList triggers = new BindableList(); - private readonly Container triggers; + public IBindableList Triggers => triggers; - public IReadOnlyList Triggers => triggers; - - public InputCountController() - { - InternalChild = triggers = new Container(); - } + public void AddRange(IEnumerable triggers) => triggers.ForEach(Add); public void Add(InputTrigger trigger) { + // Note that these triggers are not added to the hierarchy here. It is presumed they are added externally at a + // more correct location (ie. inside a RulesetInputManager). triggers.Add(trigger); trigger.IsCounting.BindTo(IsCounting); - OnNewTrigger?.Invoke(trigger); } - public void AddRange(IEnumerable inputTriggers) => inputTriggers.ForEach(Add); - public override bool HandleNonPositionalInput => true; public override bool HandlePositionalInput => true; } diff --git a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs index e222099c63..e7e866932e 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs @@ -1,11 +1,9 @@ // 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.Collections.Specialized; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Rulesets.UI; @@ -24,35 +22,17 @@ namespace osu.Game.Screens.Play.HUD /// public Bindable AlwaysVisible { get; } = new Bindable(true); - /// - /// The s contained in this . - /// - public IEnumerable Counters => KeyFlow; - protected abstract FillFlowContainer KeyFlow { get; } protected readonly Bindable ConfigVisibility = new Bindable(); + private readonly IBindableList triggers = new BindableList(); + [Resolved] private InputCountController controller { get; set; } = null!; protected abstract void UpdateVisibility(); - /// - /// Add a to this display. - /// - public void Add(InputTrigger trigger) - { - var keyCounter = CreateCounter(trigger); - - KeyFlow.Add(keyCounter); - } - - /// - /// Add a range of to this display. - /// - public void AddRange(IEnumerable triggers) => triggers.ForEach(Add); - protected abstract KeyCounter CreateCounter(InputTrigger trigger); [BackgroundDependencyLoader] @@ -68,19 +48,18 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - controller.OnNewTrigger += Add; - AddRange(controller.Triggers); + triggers.BindTo(controller.Triggers); + triggers.BindCollectionChanged(triggersChanged, true); AlwaysVisible.BindValueChanged(_ => UpdateVisibility()); ConfigVisibility.BindValueChanged(_ => UpdateVisibility(), true); } - protected override void Dispose(bool isDisposing) + private void triggersChanged(object? sender, NotifyCollectionChangedEventArgs e) { - base.Dispose(isDisposing); - - if (controller.IsNotNull()) - controller.OnNewTrigger -= Add; + KeyFlow.Clear(); + foreach (var trigger in controller.Triggers) + KeyFlow.Add(CreateCounter(trigger)); } public bool UsesFixedAnchor { get; set; } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index ae9c6a7d87..278c7b9de2 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -111,9 +111,6 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both; - // intentionally not added to hierarchy here as it will be attached via `BindDrawableRuleset()`. - InputCountController = new InputCountController(); - Children = new[] { CreateFailingLayer(), @@ -161,6 +158,7 @@ namespace osu.Game.Screens.Play Spacing = new Vector2(5) }, clicksPerSecondCalculator = new ClicksPerSecondCalculator(), + InputCountController = new InputCountController(), }; hideTargets = new List { mainComponents, rulesetComponents, topRightElements }; From 113b570bd4473341a695c9848fb1b93f2a6572e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Jun 2023 16:37:22 +0900 Subject: [PATCH 0414/2100] Move controllers above skinnable elements in initialisation order --- osu.Game/Screens/Play/HUDOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 278c7b9de2..339f1483bc 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -116,6 +116,8 @@ namespace osu.Game.Screens.Play CreateFailingLayer(), //Needs to be initialized before skinnable drawables. judgementCountController = new JudgementCountController(), + clicksPerSecondCalculator = new ClicksPerSecondCalculator(), + InputCountController = new InputCountController(), mainComponents = new HUDComponentsContainer { AlwaysPresent = true, }, rulesetComponents = drawableRuleset != null ? new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, } @@ -157,8 +159,6 @@ namespace osu.Game.Screens.Play Padding = new MarginPadding(44), // enough margin to avoid the hit error display Spacing = new Vector2(5) }, - clicksPerSecondCalculator = new ClicksPerSecondCalculator(), - InputCountController = new InputCountController(), }; hideTargets = new List { mainComponents, rulesetComponents, topRightElements }; From de23a4691ed82b766f5e3b49f01639410f4d85b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Jun 2023 16:38:15 +0900 Subject: [PATCH 0415/2100] Change `JudgementCountController` to a `Component` --- .../Play/HUD/JudgementCounter/JudgementCountController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs index 98e74a0e7e..43c2ae442a 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter /// Keeps track of judgements for a current play session, exposing bindable counts which can /// be used for display purposes. /// - public partial class JudgementCountController : CompositeDrawable + public partial class JudgementCountController : Component { [Resolved] private ScoreProcessor scoreProcessor { get; set; } = null!; From 8bd6f7a46a745da8138bdbae0a889fdfb66c6d1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Jun 2023 16:38:46 +0900 Subject: [PATCH 0416/2100] Rename `ClicksPerSecondCalculator` to `ClicksPerSecondController` --- .../Gameplay/TestSceneClicksPerSecondCalculator.cs | 10 +++++----- osu.Game/Rulesets/UI/DrawableRuleset.cs | 4 ++-- osu.Game/Rulesets/UI/ICanAttachHUDPieces.cs | 2 +- osu.Game/Rulesets/UI/RulesetInputManager.cs | 10 +++++----- ...econdCalculator.cs => ClicksPerSecondController.cs} | 4 ++-- .../Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs | 4 ++-- osu.Game/Screens/Play/HUDOverlay.cs | 6 +++--- 7 files changed, 20 insertions(+), 20 deletions(-) rename osu.Game/Screens/Play/HUD/ClicksPerSecond/{ClicksPerSecondCalculator.cs => ClicksPerSecondController.cs} (93%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs index 6b8e0e1088..bcb5291108 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs @@ -17,7 +17,7 @@ namespace osu.Game.Tests.Visual.Gameplay { public partial class TestSceneClicksPerSecondCalculator : OsuTestScene { - private ClicksPerSecondCalculator calculator = null!; + private ClicksPerSecondController controller = null!; private TestGameplayClock manualGameplayClock = null!; @@ -34,11 +34,11 @@ namespace osu.Game.Tests.Visual.Gameplay CachedDependencies = new (Type, object)[] { (typeof(IGameplayClock), manualGameplayClock) }, Children = new Drawable[] { - calculator = new ClicksPerSecondCalculator(), + controller = new ClicksPerSecondController(), new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, - CachedDependencies = new (Type, object)[] { (typeof(ClicksPerSecondCalculator), calculator) }, + CachedDependencies = new (Type, object)[] { (typeof(ClicksPerSecondController), controller) }, Child = new ClicksPerSecondCounter { Anchor = Anchor.Centre, @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.Gameplay checkClicksPerSecondValue(6); } - private void checkClicksPerSecondValue(int i) => AddAssert("clicks/s is correct", () => calculator.Value, () => Is.EqualTo(i)); + private void checkClicksPerSecondValue(int i) => AddAssert("clicks/s is correct", () => controller.Value, () => Is.EqualTo(i)); private void seekClockImmediately(double time) => manualGameplayClock.CurrentTime = time; @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Gameplay foreach (double timestamp in inputs) { seekClockImmediately(timestamp); - calculator.AddInputTimestamp(); + controller.AddInputTimestamp(); } seekClockImmediately(baseTime); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 326c77e94c..4aeb3d4862 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -338,8 +338,8 @@ namespace osu.Game.Rulesets.UI public void Attach(InputCountController inputCountController) => (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(inputCountController); - public void Attach(ClicksPerSecondCalculator calculator) => - (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(calculator); + public void Attach(ClicksPerSecondController controller) => + (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(controller); /// /// Creates a key conversion input manager. An exception will be thrown if a valid is not returned. diff --git a/osu.Game/Rulesets/UI/ICanAttachHUDPieces.cs b/osu.Game/Rulesets/UI/ICanAttachHUDPieces.cs index 1f93d25720..276881d17a 100644 --- a/osu.Game/Rulesets/UI/ICanAttachHUDPieces.cs +++ b/osu.Game/Rulesets/UI/ICanAttachHUDPieces.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.UI public interface ICanAttachHUDPieces { void Attach(InputCountController inputCountController); - void Attach(ClicksPerSecondCalculator calculator); + void Attach(ClicksPerSecondController controller); } } diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 9be210f4b2..26b9d06f73 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -177,20 +177,20 @@ namespace osu.Game.Rulesets.UI #region Keys per second Counter Attachment - public void Attach(ClicksPerSecondCalculator calculator) => KeyBindingContainer.Add(new ActionListener(calculator)); + public void Attach(ClicksPerSecondController controller) => KeyBindingContainer.Add(new ActionListener(controller)); private partial class ActionListener : Component, IKeyBindingHandler { - private readonly ClicksPerSecondCalculator calculator; + private readonly ClicksPerSecondController controller; - public ActionListener(ClicksPerSecondCalculator calculator) + public ActionListener(ClicksPerSecondController controller) { - this.calculator = calculator; + this.controller = controller; } public bool OnPressed(KeyBindingPressEvent e) { - calculator.AddInputTimestamp(); + controller.AddInputTimestamp(); return false; } diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondController.cs similarity index 93% rename from osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs rename to osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondController.cs index ba0c47dc8b..f2dd20cc8e 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondController.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Play.HUD.ClicksPerSecond { - public partial class ClicksPerSecondCalculator : Component + public partial class ClicksPerSecondController : Component { private readonly List timestamps = new List(); @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Play.HUD.ClicksPerSecond private IGameplayClock clock => frameStableClock ?? gameplayClock; - public ClicksPerSecondCalculator() + public ClicksPerSecondController() { RelativeSizeAxes = Axes.Both; } diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs index 1aa7c5e091..9b5ea309b0 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Play.HUD.ClicksPerSecond public partial class ClicksPerSecondCounter : RollingCounter, ISerialisableDrawable { [Resolved] - private ClicksPerSecondCalculator calculator { get; set; } = null!; + private ClicksPerSecondController controller { get; set; } = null!; protected override double RollingDuration => 350; @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Play.HUD.ClicksPerSecond { base.Update(); - Current.Value = calculator.Value; + Current.Value = controller.Value; } protected override IHasText CreateText() => new TextComponent(); diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 339f1483bc..f0a2975958 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Play public readonly PlayerSettingsOverlay PlayerSettingsOverlay; [Cached] - private readonly ClicksPerSecondCalculator clicksPerSecondCalculator; + private readonly ClicksPerSecondController clicksPerSecondController; [Cached] public readonly InputCountController InputCountController; @@ -116,7 +116,7 @@ namespace osu.Game.Screens.Play CreateFailingLayer(), //Needs to be initialized before skinnable drawables. judgementCountController = new JudgementCountController(), - clicksPerSecondCalculator = new ClicksPerSecondCalculator(), + clicksPerSecondController = new ClicksPerSecondController(), InputCountController = new InputCountController(), mainComponents = new HUDComponentsContainer { AlwaysPresent = true, }, rulesetComponents = drawableRuleset != null @@ -322,7 +322,7 @@ namespace osu.Game.Screens.Play if (drawableRuleset is ICanAttachHUDPieces attachTarget) { attachTarget.Attach(InputCountController); - attachTarget.Attach(clicksPerSecondCalculator); + attachTarget.Attach(clicksPerSecondController); } replayLoaded.BindTo(drawableRuleset.HasReplayLoaded); From 41890cfc65fd28e7f9e18ea586ff860c8a69ac3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Jun 2023 16:39:21 +0900 Subject: [PATCH 0417/2100] Change `JudgementCountController` to a `Component` and remove handling overrides --- osu.Game/Screens/Play/HUD/InputCountController.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/InputCountController.cs b/osu.Game/Screens/Play/HUD/InputCountController.cs index 4827f2315f..cfe17d8ce0 100644 --- a/osu.Game/Screens/Play/HUD/InputCountController.cs +++ b/osu.Game/Screens/Play/HUD/InputCountController.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; namespace osu.Game.Screens.Play.HUD { @@ -12,7 +12,7 @@ namespace osu.Game.Screens.Play.HUD /// Keeps track of key press counts for a current play session, exposing bindable counts which can /// be used for display purposes. /// - public partial class InputCountController : CompositeComponent + public partial class InputCountController : Component { public readonly Bindable IsCounting = new BindableBool(true); @@ -29,8 +29,5 @@ namespace osu.Game.Screens.Play.HUD triggers.Add(trigger); trigger.IsCounting.BindTo(IsCounting); } - - public override bool HandleNonPositionalInput => true; - public override bool HandlePositionalInput => true; } } From 7ddbf4eaa7bfb01667bf9f6bbf16048e0499546f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Jun 2023 17:01:06 +0900 Subject: [PATCH 0418/2100] Add a visual effect when keyboard shortcuts are used to trigger selection box buttons --- .../Edit/Compose/Components/SelectionBox.cs | 19 +++++++------------ .../Compose/Components/SelectionBoxButton.cs | 8 ++++---- .../Compose/Components/SelectionBoxControl.cs | 8 ++++---- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 1c5faed0e5..50eb3a186d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -32,6 +32,8 @@ namespace osu.Game.Screens.Edit.Compose.Components public Action OperationStarted; public Action OperationEnded; + private SelectionBoxButton reverseButton; + private bool canReverse; /// @@ -166,19 +168,10 @@ namespace osu.Game.Screens.Edit.Compose.Components if (e.Repeat || !e.ControlPressed) return false; - bool runOperationFromHotkey(Func operation) - { - operationStarted(); - bool result = operation?.Invoke() ?? false; - operationEnded(); - - return result; - } - switch (e.Key) { case Key.G: - return CanReverse && runOperationFromHotkey(OnReverse); + return reverseButton?.TriggerClick() ?? false; } return base.OnKeyDown(e); @@ -256,7 +249,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (CanFlipX) addXFlipComponents(); if (CanFlipY) addYFlipComponents(); if (CanRotate) addRotationComponents(); - if (CanReverse) addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke()); + if (CanReverse) reverseButton = addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke()); } private void addRotationComponents() @@ -300,7 +293,7 @@ namespace osu.Game.Screens.Edit.Compose.Components addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically", () => OnFlip?.Invoke(Direction.Vertical, false)); } - private void addButton(IconUsage icon, string tooltip, Action action) + private SelectionBoxButton addButton(IconUsage icon, string tooltip, Action action) { var button = new SelectionBoxButton(icon, tooltip) { @@ -310,6 +303,8 @@ namespace osu.Game.Screens.Edit.Compose.Components button.OperationStarted += operationStarted; button.OperationEnded += operationEnded; buttons.Add(button); + + return button; } private void addScaleHandle(Anchor anchor) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs index 832d8b65e5..6108d44c81 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -17,11 +15,11 @@ namespace osu.Game.Screens.Edit.Compose.Components { public sealed partial class SelectionBoxButton : SelectionBoxControl, IHasTooltip { - private SpriteIcon icon; + private SpriteIcon icon = null!; private readonly IconUsage iconUsage; - public Action Action; + public Action? Action; public SelectionBoxButton(IconUsage iconUsage, string tooltip) { @@ -49,6 +47,8 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnClick(ClickEvent e) { + Circle.FlashColour(Colours.GrayF, 300); + TriggerOperationStarted(); Action?.Invoke(); TriggerOperationEnded(); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxControl.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxControl.cs index 35c67a1c67..3746c9652e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxControl.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public event Action OperationStarted; public event Action OperationEnded; - private Circle circle; + protected Circle Circle { get; private set; } /// /// Whether the user is currently holding the control with mouse. @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit.Compose.Components InternalChildren = new Drawable[] { - circle = new Circle + Circle = new Circle { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -85,9 +85,9 @@ namespace osu.Game.Screens.Edit.Compose.Components protected virtual void UpdateHoverState() { if (IsHeld) - circle.FadeColour(Colours.GrayF, TRANSFORM_DURATION, Easing.OutQuint); + Circle.FadeColour(Colours.GrayF, TRANSFORM_DURATION, Easing.OutQuint); else - circle.FadeColour(IsHovered ? Colours.Red : Colours.YellowDark, TRANSFORM_DURATION, Easing.OutQuint); + Circle.FadeColour(IsHovered ? Colours.Red : Colours.YellowDark, TRANSFORM_DURATION, Easing.OutQuint); this.ScaleTo(IsHeld || IsHovered ? 1.5f : 1, TRANSFORM_DURATION, Easing.OutQuint); } From c6d952abe36a1ae16224291cd60d3012ef709140 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Jun 2023 17:01:40 +0900 Subject: [PATCH 0419/2100] Add support for `Ctrl` + `<` / `>` to rotate selection in editor As discussed in https://github.com/ppy/osu/discussions/24048. --- .../Screens/Edit/Compose/Components/SelectionBox.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 50eb3a186d..d8fd18ff8f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -33,6 +33,8 @@ namespace osu.Game.Screens.Edit.Compose.Components public Action OperationEnded; private SelectionBoxButton reverseButton; + private SelectionBoxButton rotateClockwiseButton; + private SelectionBoxButton rotateCounterClockwiseButton; private bool canReverse; @@ -172,6 +174,12 @@ namespace osu.Game.Screens.Edit.Compose.Components { case Key.G: return reverseButton?.TriggerClick() ?? false; + + case Key.Comma: + return rotateCounterClockwiseButton?.TriggerClick() ?? false; + + case Key.Period: + return rotateClockwiseButton?.TriggerClick() ?? false; } return base.OnKeyDown(e); @@ -254,8 +262,8 @@ namespace osu.Game.Screens.Edit.Compose.Components private void addRotationComponents() { - addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise", () => OnRotation?.Invoke(-90)); - addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise", () => OnRotation?.Invoke(90)); + rotateCounterClockwiseButton = addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise", () => OnRotation?.Invoke(-90)); + rotateClockwiseButton = addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise", () => OnRotation?.Invoke(90)); addRotateHandle(Anchor.TopLeft); addRotateHandle(Anchor.TopRight); From 5f350aa66fc118b563b2e9ca98f80f40245ca7fc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 27 Jun 2023 16:47:42 +0900 Subject: [PATCH 0420/2100] Fix float division Firstly, this is intended to be a float division. Secondly, dividing integers by 0 results in an exception, but dividing non-zero floats by 0 results in +/- infinity which will be clamped to the upper range. In particular, this occurs when the beatmap has 1 hitobject (0 drain length). --- osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs | 2 +- osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs index bda6be66a4..b4cca610c3 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty (baseBeatmap.Difficulty.DrainRate + baseBeatmap.Difficulty.OverallDifficulty + baseBeatmap.Difficulty.CircleSize - + Math.Clamp(objectCount / drainLength * 8, 0, 16)) / 38 * 5); + + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5); scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs index aa52edae87..2e40d03fc0 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs @@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty (baseBeatmap.Difficulty.DrainRate + baseBeatmap.Difficulty.OverallDifficulty + baseBeatmap.Difficulty.CircleSize - + Math.Clamp(objectCount / drainLength * 8, 0, 16)) / 38 * 5); + + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5); scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs index 255a3dd963..eaa82e695e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty (baseBeatmap.Difficulty.DrainRate + baseBeatmap.Difficulty.OverallDifficulty + baseBeatmap.Difficulty.CircleSize - + Math.Clamp(objectCount / drainLength * 8, 0, 16)) / 38 * 5); + + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5); modMultiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); From 6e2369e6516ee3db92b9226f8a633d9c96b97773 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 27 Jun 2023 17:18:32 +0900 Subject: [PATCH 0421/2100] Add xmldoc on LegacyTotalScore --- osu.Game/Scoring/ScoreInfo.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 5de1c69d8a..a0a0799fb1 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -54,6 +54,9 @@ namespace osu.Game.Scoring public long TotalScore { get; set; } + /// + /// Used to preserve the total score for legacy scores. + /// public long LegacyTotalScore { get; set; } public int MaxCombo { get; set; } From 4ecc724841ac075f24f8d5695fb277672ccceca8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Jun 2023 18:18:58 +0900 Subject: [PATCH 0422/2100] Add test coverage of save failure when beatmap is detached from set --- .../Visual/Editing/TestSceneEditorSaving.cs | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index 64c48e74cf..7191ae6a57 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -96,6 +94,33 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("Beatmap still has correct timeline zoom", () => EditorBeatmap.BeatmapInfo.TimelineZoom == changedTimelineZoom); } + [Test] + public void TestSaveWithDetachedBeatmap() + { + AddStep("Set overall difficulty", () => EditorBeatmap.Difficulty.OverallDifficulty = 7); + + SaveEditor(); + + AddStep("Detach beatmap from set", () => + { + Realm.Write(r => + { + BeatmapSetInfo? beatmapSet = r.Find(EditorBeatmap.BeatmapInfo.BeatmapSet!.ID); + BeatmapInfo? beatmap = r.Find(EditorBeatmap.BeatmapInfo.ID); + + beatmapSet.Beatmaps.Remove(beatmap); + }); + }); + + SaveEditor(); + + AddAssert("Beatmap has correct overall difficulty", () => EditorBeatmap.Difficulty.OverallDifficulty == 7); + + ReloadEditorToSameBeatmap(); + + AddAssert("Beatmap still has correct overall difficulty", () => EditorBeatmap.Difficulty.OverallDifficulty == 7); + } + [Test] public void TestDifficulty() { @@ -130,7 +155,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestLengthAndStarRatingUpdated() { - WorkingBeatmap working = null; + WorkingBeatmap working = null!; double lastStarRating = 0; double lastLength = 0; From 8e80e2fa323337f885ae8119a5a14b5b54e68596 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Jun 2023 18:19:59 +0900 Subject: [PATCH 0423/2100] Fix incorrect realm copy logic when a beatmap becomes detached from its set The code here was assuming that if the beatmap which is having changes copied across does not exist within the `BeatmapSet.Beatmaps` list, it was not yet persisted to realm. In some edge case, it can happen that the beatmap *is* persisted to realm but not correctly attached to the beatmap set. I don't yet know how this occurs, but it has caused loss of data for at least two users. The fix here is to check realm-wide for the beatmap (using its primary key) rather than only in the list. We then handle the scenario where the beatmap needs to be reattached to the set as a seprate step. --- This does raise others questions like "are we even structuring this correctly? couldn't a single beatmap exist in two different sets?" Maybe, but let's deal with that if/when it comes up. --- osu.Game/Database/RealmObjectExtensions.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index a771aa04df..888c1cf8ce 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -52,10 +52,19 @@ namespace osu.Game.Database { foreach (var beatmap in s.Beatmaps) { - var existing = d.Beatmaps.FirstOrDefault(b => b.ID == beatmap.ID); + // Importantly, search all of realm for the beatmap (not just the set's beatmaps). + // It may have gotten detached, and if that's the case let's use this opportunity to fix + // things up. + var existingBeatmap = d.Realm.Find(beatmap.ID); - if (existing != null) - copyChangesToRealm(beatmap, existing); + if (existingBeatmap != null) + { + // As above, reattach if it happens to not be in the set's beatmaps. + if (!d.Beatmaps.Contains(existingBeatmap)) + d.Beatmaps.Add(existingBeatmap); + + copyChangesToRealm(beatmap, existingBeatmap); + } else { var newBeatmap = new BeatmapInfo @@ -64,6 +73,7 @@ namespace osu.Game.Database BeatmapSet = d, Ruleset = d.Realm.Find(beatmap.Ruleset.ShortName) }; + d.Beatmaps.Add(newBeatmap); copyChangesToRealm(beatmap, newBeatmap); } From ada9c48bde241592c4765ed90040b66a0f67746f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Jun 2023 20:14:33 +0200 Subject: [PATCH 0424/2100] Attempt to fix more test failures --- osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index be73e36e11..4d81d9f707 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -63,7 +63,6 @@ namespace osu.Game.Tests.Visual.Gameplay float? initialAlpha = null; createNew(h => h.OnLoadComplete += _ => initialAlpha = hideTarget.Alpha); - AddUntilStep("wait for load", () => hudOverlay.IsAlive); AddAssert("initial alpha was less than 1", () => initialAlpha < 1); } @@ -97,6 +96,7 @@ namespace osu.Game.Tests.Visual.Gameplay return hudOverlay; }); }); + AddUntilStep("wait for load", () => hudOverlay.IsAlive); } protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); From 9681ee7eeb0d7dee7ddb44ee1a56c2afa593fd23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Jun 2023 20:29:27 +0200 Subject: [PATCH 0425/2100] Fix broken test step --- osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 3df0f18558..5a66a5c7a6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -62,7 +62,9 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Add random", () => { Key key = (Key)((int)Key.A + RNG.Next(26)); - controller.Add(new KeyCounterKeyboardTrigger(key)); + var trigger = new KeyCounterKeyboardTrigger(key); + Add(trigger); + controller.Add(trigger); }); InputTrigger testTrigger = controller.Triggers.First(); From 11577d1df08e96008ec9764ea731c5747ae20996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Jun 2023 20:41:03 +0200 Subject: [PATCH 0426/2100] Add test coverage for title query parsing --- .../NonVisual/Filtering/FilterQueryParserTest.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index 46f91f9a67..9f7ba9ac24 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -251,6 +251,18 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual("my_fav", filterCriteria.Creator.SearchTerm); } + [Test] + public void TestApplyTitleQueries() + { + const string query = "find me songs with title=\"a certain title\" please"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("find me songs with please", filterCriteria.SearchText.Trim()); + Assert.AreEqual(5, filterCriteria.SearchTerms.Length); + Assert.AreEqual("a certain title", filterCriteria.Title.SearchTerm); + Assert.That(filterCriteria.Title.Exact, Is.True); + } + [Test] public void TestApplyArtistQueries() { From 37ee3a7bbd0f1f14e03a87e29b83abd05c800e18 Mon Sep 17 00:00:00 2001 From: Bastian Pedersen Date: Tue, 27 Jun 2023 20:56:35 +0200 Subject: [PATCH 0427/2100] Localise common game notifications --- osu.Game/Localisation/NotificationsStrings.cs | 25 +++++++++++++++++++ osu.Game/OsuGame.cs | 10 ++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 5e2600bc50..14ab3e7ff4 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -63,6 +63,31 @@ Please try changing your audio device to a working setting."); /// public static LocalisableString ScoreOverlayDisabled(LocalisableString arg0) => new TranslatableString(getKey(@"score_overlay_disabled"), @"The score overlay is currently disabled. You can toggle this by pressing {0}.", arg0); + /// + /// "The URL {0} has an unsupported or dangerous protocol and will not be opened" + /// + public static LocalisableString UnsupportedOrDangerousUrlProtocol(string url) => new TranslatableString(getKey(@"unsupported_or_dangerous_url_protocol"), @"The URL {0} has an unsupported or dangerous protocol and will not be opened.", url); + + /// + /// "Subsequent messages have been logged. Click to view log files" + /// + public static LocalisableString SubsequentMessagesLogged => new TranslatableString(getKey(@"subsequent_messages_logged"), @"Subsequent messages have been logged. Click to view log files"); + + /// + /// "Disabling tablet support due to error: "{0}"" + /// + public static LocalisableString TabletSupportDisabledDueToError(string message) => new TranslatableString(getKey(@"tablet_support_disabled_due_to_error"), @"Disabling tablet support due to error: ""{0}""", message); + + /// + /// "Encountered tablet warning, your tablet may not function correctly. Click here for a list of all tablets supported." + /// + public static LocalisableString EncounteredTabletWarning => new TranslatableString(getKey(@"encountered_tablet_warning"), @"Encountered tablet warning, your tablet may not function correctly. Click here for a list of all tablets supported."); + + /// + /// "This link type is not yet supported!" + /// + public static LocalisableString LinkTypeNotSupported => new TranslatableString(getKey(@"unsupported_link_type"), @"This link type is not yet supported!"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 8bfe48010b..fe98a8e286 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -435,7 +435,7 @@ namespace osu.Game case LinkAction.Spectate: waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification { - Text = @"This link type is not yet supported!", + Text = NotificationsStrings.LinkTypeNotSupported, Icon = FontAwesome.Solid.LifeRing, })); break; @@ -477,7 +477,7 @@ namespace osu.Game { Notifications.Post(new SimpleErrorNotification { - Text = $"The URL {url} has an unsupported or dangerous protocol and will not be opened.", + Text = NotificationsStrings.UnsupportedOrDangerousUrlProtocol(url), }); return; @@ -1147,7 +1147,7 @@ namespace osu.Game Schedule(() => Notifications.Post(new SimpleNotification { Icon = FontAwesome.Solid.EllipsisH, - Text = "Subsequent messages have been logged. Click to view log files.", + Text = NotificationsStrings.SubsequentMessagesLogged, Activated = () => { Storage.GetStorageForDirectory(@"logs").PresentFileExternally(logFile); @@ -1179,7 +1179,7 @@ namespace osu.Game { Notifications.Post(new SimpleNotification { - Text = $"Disabling tablet support due to error: \"{message}\"", + Text = NotificationsStrings.TabletSupportDisabledDueToError(message), Icon = FontAwesome.Solid.PenSquare, IconColour = Colours.RedDark, }); @@ -1196,7 +1196,7 @@ namespace osu.Game { Schedule(() => Notifications.Post(new SimpleNotification { - Text = @"Encountered tablet warning, your tablet may not function correctly. Click here for a list of all tablets supported.", + Text = NotificationsStrings.EncounteredTabletWarning, Icon = FontAwesome.Solid.PenSquare, IconColour = Colours.YellowDark, Activated = () => From 8a2cd57f4eeca358d9f1b7bbc0042ae441fb23bb Mon Sep 17 00:00:00 2001 From: Bastian Pedersen Date: Tue, 27 Jun 2023 21:01:39 +0200 Subject: [PATCH 0428/2100] Add back missing punctunation --- osu.Game/Localisation/NotificationsStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 14ab3e7ff4..f568c47546 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -69,9 +69,9 @@ Please try changing your audio device to a working setting."); public static LocalisableString UnsupportedOrDangerousUrlProtocol(string url) => new TranslatableString(getKey(@"unsupported_or_dangerous_url_protocol"), @"The URL {0} has an unsupported or dangerous protocol and will not be opened.", url); /// - /// "Subsequent messages have been logged. Click to view log files" + /// "Subsequent messages have been logged. Click to view log files." /// - public static LocalisableString SubsequentMessagesLogged => new TranslatableString(getKey(@"subsequent_messages_logged"), @"Subsequent messages have been logged. Click to view log files"); + public static LocalisableString SubsequentMessagesLogged => new TranslatableString(getKey(@"subsequent_messages_logged"), @"Subsequent messages have been logged. Click to view log files."); /// /// "Disabling tablet support due to error: "{0}"" From 62dcd513caa6fd2e566902f503c49636598a9645 Mon Sep 17 00:00:00 2001 From: Bastian Pedersen Date: Tue, 27 Jun 2023 21:02:44 +0200 Subject: [PATCH 0429/2100] Fix XML doc not mirroring string --- osu.Game/Localisation/NotificationsStrings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index f568c47546..b6f2a55e37 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -64,7 +64,7 @@ Please try changing your audio device to a working setting."); public static LocalisableString ScoreOverlayDisabled(LocalisableString arg0) => new TranslatableString(getKey(@"score_overlay_disabled"), @"The score overlay is currently disabled. You can toggle this by pressing {0}.", arg0); /// - /// "The URL {0} has an unsupported or dangerous protocol and will not be opened" + /// "The URL {0} has an unsupported or dangerous protocol and will not be opened." /// public static LocalisableString UnsupportedOrDangerousUrlProtocol(string url) => new TranslatableString(getKey(@"unsupported_or_dangerous_url_protocol"), @"The URL {0} has an unsupported or dangerous protocol and will not be opened.", url); From 7052f87eb81d727ac54718c2eb25a33ea4458ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Jun 2023 21:20:59 +0200 Subject: [PATCH 0430/2100] Add even more safety against unloaded components --- .../Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index 4d81d9f707..162e279403 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -96,7 +96,9 @@ namespace osu.Game.Tests.Visual.Gameplay return hudOverlay; }); }); - AddUntilStep("wait for load", () => hudOverlay.IsAlive); + AddUntilStep("HUD overlay loaded", () => hudOverlay.IsAlive); + AddUntilStep("components container loaded", + () => hudOverlay.ChildrenOfType().Any(scc => scc.ComponentsLoaded)); } protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); From e3d97b37f120e24e96e81b41faba96b6c5898e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Jun 2023 21:28:37 +0200 Subject: [PATCH 0431/2100] Rename `MatchMode.{None -> Substring}` --- .../NonVisual/Filtering/FilterQueryParserTest.cs | 8 ++++---- osu.Game/Screens/Select/FilterCriteria.cs | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index 7e7f7c95fe..ade8146774 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -39,13 +39,13 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.That(filterCriteria.SearchTerms[1].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase)); Assert.That(filterCriteria.SearchTerms[2].SearchTerm, Is.EqualTo("looking")); - Assert.That(filterCriteria.SearchTerms[2].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.None)); + Assert.That(filterCriteria.SearchTerms[2].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring)); Assert.That(filterCriteria.SearchTerms[3].SearchTerm, Is.EqualTo("for")); - Assert.That(filterCriteria.SearchTerms[3].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.None)); + Assert.That(filterCriteria.SearchTerms[3].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring)); Assert.That(filterCriteria.SearchTerms[4].SearchTerm, Is.EqualTo("like")); - Assert.That(filterCriteria.SearchTerms[4].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.None)); + Assert.That(filterCriteria.SearchTerms[4].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring)); } /* @@ -272,7 +272,7 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual("find me songs by please", filterCriteria.SearchText.Trim()); Assert.AreEqual(5, filterCriteria.SearchTerms.Length); Assert.AreEqual("singer", filterCriteria.Artist.SearchTerm); - Assert.That(filterCriteria.Artist.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.None)); + Assert.That(filterCriteria.Artist.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring)); } [Test] diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 6ac6b7d5fe..36e048fd3d 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -154,7 +154,7 @@ namespace osu.Game.Screens.Select switch (MatchMode) { default: - case MatchMode.None: + case MatchMode.Substring: return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase); case MatchMode.IsolatedPhrase: @@ -185,7 +185,7 @@ namespace osu.Game.Screens.Select MatchMode = MatchMode.IsolatedPhrase; } else - MatchMode = MatchMode.None; + MatchMode = MatchMode.Substring; } } @@ -197,7 +197,7 @@ namespace osu.Game.Screens.Select /// /// Match using a simple "contains" substring match. /// - None, + Substring, /// /// Match for the search phrase being isolated by spaces, or at the start or end of the text. From aba380b0018770d31264dce4d886e3fc7a61005a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Jun 2023 21:33:42 +0200 Subject: [PATCH 0432/2100] Add test case for full phrase match mode trimming chars from inside phrase --- .../Filtering/FilterQueryParserTest.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index ade8146774..be8b39b296 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -48,6 +48,38 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.That(filterCriteria.SearchTerms[4].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring)); } + [Test] + public void TestApplyFullPhraseQueryWithExclamationPointInTerm() + { + const string query = "looking for \"circles!\"!"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("looking for \"circles!\"!", filterCriteria.SearchText); + Assert.AreEqual(3, filterCriteria.SearchTerms.Length); + + Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("circles!")); + Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.FullPhrase)); + + Assert.That(filterCriteria.SearchTerms[1].SearchTerm, Is.EqualTo("looking")); + Assert.That(filterCriteria.SearchTerms[1].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring)); + + Assert.That(filterCriteria.SearchTerms[2].SearchTerm, Is.EqualTo("for")); + Assert.That(filterCriteria.SearchTerms[2].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring)); + } + + [Test] + public void TestApplyBrokenFullPhraseQuery() + { + const string query = "\"!"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("\"!", filterCriteria.SearchText); + Assert.AreEqual(1, filterCriteria.SearchTerms.Length); + + Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("\"!")); + Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring)); + } + /* * The following tests have been written a bit strangely (they don't check exact * bound equality with what the filter says). From bf99fc61b863761b08e40345fcbb8d8eb287a771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Jun 2023 21:34:49 +0200 Subject: [PATCH 0433/2100] Trim full phrase filters in a more precise manner --- .../Filtering/FilterQueryParserTest.cs | 4 ++-- osu.Game/Screens/Select/FilterCriteria.cs | 20 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index be8b39b296..ce95e921b9 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -76,8 +76,8 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual("\"!", filterCriteria.SearchText); Assert.AreEqual(1, filterCriteria.SearchTerms.Length); - Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("\"!")); - Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring)); + Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("!")); + Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase)); } /* diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 36e048fd3d..ab4f85fc92 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -174,15 +174,19 @@ namespace osu.Game.Screens.Select { searchTerm = value; - if (searchTerm.EndsWith("\"!", StringComparison.Ordinal)) + if (searchTerm.StartsWith('\"')) { - searchTerm = searchTerm.Trim('!', '\"'); - MatchMode = MatchMode.FullPhrase; - } - else if (searchTerm.StartsWith('\"')) - { - searchTerm = searchTerm.Trim('\"'); - MatchMode = MatchMode.IsolatedPhrase; + // length check ensures that the quote character in the `StartsWith()` check above and the `EndsWith()` check below is not the same character. + if (searchTerm.EndsWith("\"!", StringComparison.Ordinal) && searchTerm.Length >= 3) + { + searchTerm = searchTerm.TrimEnd('!').Trim('\"'); + MatchMode = MatchMode.FullPhrase; + } + else + { + searchTerm = searchTerm.Trim('\"'); + MatchMode = MatchMode.IsolatedPhrase; + } } else MatchMode = MatchMode.Substring; From bca1a910878cf48577d43bc72f92d7741fea038f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Jun 2023 21:38:36 +0200 Subject: [PATCH 0434/2100] Add test cases covering full phrase case insensitivity --- osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index 44cff7fdd9..c7a32ebbc4 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -171,6 +171,7 @@ namespace osu.Game.Tests.NonVisual.Filtering [TestCase("\"an auteur\"", true)] [TestCase("\"Artist\"!", true)] [TestCase("\"The Artist\"!", false)] + [TestCase("\"the artist\"!", false)] [TestCase("\"\\\"", true)] // nasty case, covers properly escaping user input in underlying regex. public void TestCriteriaMatchingExactTerms(string terms, bool filtered) { @@ -238,6 +239,7 @@ namespace osu.Game.Tests.NonVisual.Filtering [TestCase("unknown", true)] [TestCase("\"Artist\"!", true)] [TestCase("\"The Artist\"!", false)] + [TestCase("\"the artist\"!", false)] public void TestCriteriaMatchingArtist(string artistName, bool filtered) { var exampleBeatmapInfo = getExampleBeatmap(); From ad3a470eafad8a22b678a45d6ff7a7da574764c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Jun 2023 22:01:44 +0200 Subject: [PATCH 0435/2100] Enable NRT in `SelectionBox` --- .../Edit/Compose/Components/SelectionBox.cs | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index d8fd18ff8f..8ab2a821a9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -24,17 +22,17 @@ namespace osu.Game.Screens.Edit.Compose.Components private const float button_padding = 5; - public Func OnRotation; - public Func OnScale; - public Func OnFlip; - public Func OnReverse; + public Func? OnRotation; + public Func? OnScale; + public Func? OnFlip; + public Func? OnReverse; - public Action OperationStarted; - public Action OperationEnded; + public Action? OperationStarted; + public Action? OperationEnded; - private SelectionBoxButton reverseButton; - private SelectionBoxButton rotateClockwiseButton; - private SelectionBoxButton rotateCounterClockwiseButton; + private SelectionBoxButton? reverseButton; + private SelectionBoxButton? rotateClockwiseButton; + private SelectionBoxButton? rotateCounterClockwiseButton; private bool canReverse; @@ -138,7 +136,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - private string text; + private string text = string.Empty; public string Text { @@ -154,13 +152,13 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - private SelectionBoxDragHandleContainer dragHandles; - private FillFlowContainer buttons; + private SelectionBoxDragHandleContainer dragHandles = null!; + private FillFlowContainer buttons = null!; - private OsuSpriteText selectionDetailsText; + private OsuSpriteText? selectionDetailsText; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [BackgroundDependencyLoader] private void load() => recreate(); From 54280f06be5d8cb0dae7348061088278bbfd9a7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Jun 2023 22:02:15 +0200 Subject: [PATCH 0436/2100] Switch to `== true` --- osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 8ab2a821a9..81b501b39e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -171,13 +171,13 @@ namespace osu.Game.Screens.Edit.Compose.Components switch (e.Key) { case Key.G: - return reverseButton?.TriggerClick() ?? false; + return reverseButton?.TriggerClick() == true; case Key.Comma: - return rotateCounterClockwiseButton?.TriggerClick() ?? false; + return rotateCounterClockwiseButton?.TriggerClick() == true; case Key.Period: - return rotateClockwiseButton?.TriggerClick() ?? false; + return rotateClockwiseButton?.TriggerClick() == true; } return base.OnKeyDown(e); From 17ed45d07c2e8b6fba9995fc65a4963a613341ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Jun 2023 22:04:15 +0200 Subject: [PATCH 0437/2100] Mention hotkeys in button tooltips --- osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 81b501b39e..05fe137732 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -260,8 +260,8 @@ namespace osu.Game.Screens.Edit.Compose.Components private void addRotationComponents() { - rotateCounterClockwiseButton = addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise", () => OnRotation?.Invoke(-90)); - rotateClockwiseButton = addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise", () => OnRotation?.Invoke(90)); + rotateCounterClockwiseButton = addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise (Ctrl-<)", () => OnRotation?.Invoke(-90)); + rotateClockwiseButton = addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise (Ctrl->)", () => OnRotation?.Invoke(90)); addRotateHandle(Anchor.TopLeft); addRotateHandle(Anchor.TopRight); From 444f71541ad8894077674b7a350faf3fe7993c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Jun 2023 22:10:53 +0200 Subject: [PATCH 0438/2100] Add test coverage for rotate hotkeys --- .../Editing/TestSceneComposerSelection.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index b14025c9d8..4d99c47f77 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -101,6 +101,38 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 100); } + [Test] + public void TestRotateHotkeys() + { + HitCircle[] addedObjects = null; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] + { + new HitCircle { StartTime = 100 }, + new HitCircle { StartTime = 200, Position = new Vector2(100) }, + new HitCircle { StartTime = 300, Position = new Vector2(200) }, + new HitCircle { StartTime = 400, Position = new Vector2(300) }, + })); + + AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects)); + + AddStep("rotate clockwise", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Period); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddAssert("objects rotated clockwise", () => addedObjects[0].Position == new Vector2(300, 0)); + + AddStep("rotate counterclockwise", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Comma); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddAssert("objects reverted to original position", () => addedObjects[0].Position == new Vector2(0)); + } + [Test] public void TestBasicSelect() { From 9be2d9d62e131d22e98b650bdb82e48c97d8f18a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Jun 2023 22:25:01 +0200 Subject: [PATCH 0439/2100] Fix hotkey presses generating unnecessary undo history The buttons don't check whether the operation they correspond to is possible to perform in the current state of the selection box, so not checking `Can{Reverse,Rotate}` causes superfluous undo states to be added without any real changes if an attempt is made to reverse or rotate a selection that cannot be reversed or rotated. --- osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 05fe137732..e93b9f0691 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -171,13 +171,13 @@ namespace osu.Game.Screens.Edit.Compose.Components switch (e.Key) { case Key.G: - return reverseButton?.TriggerClick() == true; + return CanReverse && reverseButton?.TriggerClick() == true; case Key.Comma: - return rotateCounterClockwiseButton?.TriggerClick() == true; + return CanRotate && rotateCounterClockwiseButton?.TriggerClick() == true; case Key.Period: - return rotateClockwiseButton?.TriggerClick() == true; + return CanRotate && rotateClockwiseButton?.TriggerClick() == true; } return base.OnKeyDown(e); From d72a8da2951a23c526a4b9965b75871702e0a5ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Jun 2023 23:40:12 +0200 Subject: [PATCH 0440/2100] Add test coverage for deleted difficulties staying in realm --- .../Visual/Editing/TestSceneDifficultyDelete.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs index 280e6de97e..12e00c4485 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs @@ -72,9 +72,13 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null); AddStep("confirm", () => InputManager.Key(Key.Number1)); - AddAssert($"difficulty {i} is deleted", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Select(b => b.ID), () => Does.Not.Contain(deletedDifficultyID)); - AddAssert("count decreased by one", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, () => Is.EqualTo(countBeforeDeletion - 1)); + AddAssert($"difficulty {i} is unattached from set", + () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Select(b => b.ID), () => Does.Not.Contain(deletedDifficultyID)); + AddAssert("beatmap set difficulty count decreased by one", + () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, () => Is.EqualTo(countBeforeDeletion - 1)); AddAssert("set hash changed", () => Beatmap.Value.BeatmapSetInfo.Hash, () => Is.Not.EqualTo(beatmapSetHashBefore)); + AddAssert($"difficulty {i} is deleted from realm", + () => Realm.Run(r => r.Find(deletedDifficultyID)), () => Is.Null); } } } From 6876566530d842e90b2b7330cabcdaaf7c499faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Jun 2023 23:43:00 +0200 Subject: [PATCH 0441/2100] Fix difficulty deletion not deleting records from realm --- 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 305dc01844..73811b2e62 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -339,6 +339,8 @@ namespace osu.Game.Beatmaps DeleteFile(setInfo, beatmapInfo.File); setInfo.Beatmaps.Remove(beatmapInfo); + r.Remove(beatmapInfo.Metadata); + r.Remove(beatmapInfo); updateHashAndMarkDirty(setInfo); workingBeatmapCache.Invalidate(setInfo); From b3f2a3ccdfd7d6bc3f9aa01a144058ca084d3220 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 12:11:40 +0900 Subject: [PATCH 0442/2100] Use more correct localised string source for "sign out" text --- osu.Game/Overlays/Login/UserAction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Login/UserAction.cs b/osu.Game/Overlays/Login/UserAction.cs index d4d639f2fb..aa2fad6cdb 100644 --- a/osu.Game/Overlays/Login/UserAction.cs +++ b/osu.Game/Overlays/Login/UserAction.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Login [LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.AppearOffline))] AppearOffline, - [LocalisableDescription(typeof(UserVerificationStrings), nameof(UserVerificationStrings.BoxInfoLogoutLink))] + [LocalisableDescription(typeof(LayoutStrings), nameof(LayoutStrings.PopupUserLinksLogout))] SignOut, } } From 99e55bb9c03972fdee0569ee4ae7a5c37eac84a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 12:21:05 +0900 Subject: [PATCH 0443/2100] Add logging and `Debug.Fail` on detached beatmap detection --- osu.Game/Database/RealmObjectExtensions.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 888c1cf8ce..5a6c2e3232 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.Serialization; using AutoMapper; using AutoMapper.Internal; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Input.Bindings; using osu.Game.Models; @@ -61,7 +63,11 @@ namespace osu.Game.Database { // As above, reattach if it happens to not be in the set's beatmaps. if (!d.Beatmaps.Contains(existingBeatmap)) + { + Debug.Fail("Beatmaps should never become detached under normal circumstances. If this ever triggers, it should be investigated further."); + Logger.Log("WARNING: One of the difficulties in a beatmap was detached from its set. Please save a copy of logs and report this to devs.", LoggingTarget.Database, LogLevel.Important); d.Beatmaps.Add(existingBeatmap); + } copyChangesToRealm(beatmap, existingBeatmap); } From 29376ffcc0c7a52620ddbbe6096261b7ca6dd3ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 12:54:12 +0900 Subject: [PATCH 0444/2100] Trigger state change when flipping via hotkey in the editor This will trigger a change even if nothing happens. But I think that's okay (not easy to avoid) because the change handler should be aware that nothing changed, if anything. Closes https://github.com/ppy/osu/issues/24065. --- .../Edit/Compose/Components/SelectionHandler.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 5cedf1ca42..9ec59cf833 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -163,10 +163,18 @@ namespace osu.Game.Screens.Edit.Compose.Components switch (e.Action) { case GlobalAction.EditorFlipHorizontally: - return HandleFlip(Direction.Horizontal, true); + ChangeHandler?.BeginChange(); + HandleFlip(Direction.Horizontal, true); + ChangeHandler?.EndChange(); + + return true; case GlobalAction.EditorFlipVertically: - return HandleFlip(Direction.Vertical, true); + ChangeHandler?.BeginChange(); + HandleFlip(Direction.Vertical, true); + ChangeHandler?.EndChange(); + + return true; } return false; From e291dff5ad631a15e3b4beef63e785b41a574540 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 28 Jun 2023 14:50:16 +0900 Subject: [PATCH 0445/2100] Fix imported scores not getting LegacyTotalScore --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index c6461840aa..bf592d5988 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -123,6 +123,9 @@ namespace osu.Game.Scoring.Legacy PopulateAccuracy(score.ScoreInfo); + if (score.ScoreInfo.IsLegacyScore) + score.ScoreInfo.LegacyTotalScore = score.ScoreInfo.TotalScore; + // before returning for database import, we must restore the database-sourced BeatmapInfo. // if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception. score.ScoreInfo.BeatmapInfo = workingBeatmap.BeatmapInfo; From 91354b15705c5e7ea154d2229c715d7f43970c53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 14:51:19 +0900 Subject: [PATCH 0446/2100] Avoid performing any actions when `BeatmapAvailability` is updated to `Unknown` --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 978d77b4f1..ecf38a956d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -313,16 +313,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.ChangeBeatmapAvailability(availability.NewValue).FireAndForget(); - if (availability.NewValue.State != DownloadState.LocallyAvailable) + switch (availability.NewValue.State) { - // while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap. - if (client.LocalUser?.State == MultiplayerUserState.Ready) - client.ChangeState(MultiplayerUserState.Idle); - } - else if (client.LocalUser?.State == MultiplayerUserState.Spectating - && (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing)) - { - onLoadRequested(); + case DownloadState.LocallyAvailable: + if (client.LocalUser?.State == MultiplayerUserState.Spectating + && (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing)) + { + onLoadRequested(); + } + + break; + + case DownloadState.Unknown: + // Don't do anything rash in an unknown state. + break; + + default: + // while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap. + if (client.LocalUser?.State == MultiplayerUserState.Ready) + client.ChangeState(MultiplayerUserState.Idle); + break; } } From 664a2b2255b7e4e59ce6dac72ca04cca3323f528 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 14:51:41 +0900 Subject: [PATCH 0447/2100] Force a beatmap availability state change when selected item is changed --- osu.Game/Online/Rooms/BeatmapAvailability.cs | 1 + .../Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/osu.Game/Online/Rooms/BeatmapAvailability.cs b/osu.Game/Online/Rooms/BeatmapAvailability.cs index f2b981c075..a907ee0d3b 100644 --- a/osu.Game/Online/Rooms/BeatmapAvailability.cs +++ b/osu.Game/Online/Rooms/BeatmapAvailability.cs @@ -34,6 +34,7 @@ namespace osu.Game.Online.Rooms DownloadProgress = downloadProgress; } + public static BeatmapAvailability Unknown() => new BeatmapAvailability(DownloadState.Unknown); public static BeatmapAvailability NotDownloaded() => new BeatmapAvailability(DownloadState.NotDownloaded); public static BeatmapAvailability Downloading(float progress) => new BeatmapAvailability(DownloadState.Downloading, progress); public static BeatmapAvailability Importing() => new BeatmapAvailability(DownloadState.Importing); diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index cce633d46a..29f75bed97 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -60,6 +60,15 @@ namespace osu.Game.Online.Rooms if (item.NewValue == null) return; + // Initially set to unknown until we have attained a good state. + // This has the wanted side effect of forcing a state change when the current playlist + // item changes at the server but our local availability doesn't necessarily change + // (ie. we have both the previous and next item LocallyAvailable). + // + // Note that even without this, the server will trigger a state change and things will work. + // This is just for safety. + availability.Value = BeatmapAvailability.Unknown(); + downloadTracker?.RemoveAndDisposeImmediately(); selectedBeatmap = null; From 3883c28b1567eaf1e3e6286ea78fc559e8a29a02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 15:39:32 +0900 Subject: [PATCH 0448/2100] Add visual display in participants list when availability is still being established --- .../Multiplayer/TestSceneMultiplayerParticipantsList.cs | 1 + .../OnlinePlay/Multiplayer/Participants/StateDisplay.cs | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 2da29ccc95..a01d2bf9fc 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -107,6 +107,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestBeatmapDownloadingStates() { + AddStep("set to unknown", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Unknown())); AddStep("set to no map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded())); AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs index bfdc0c02ac..b0cc13d645 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs @@ -154,6 +154,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants this.FadeOut(fade_time); break; + case DownloadState.Unknown: + text.Text = "checking availability"; + icon.Icon = FontAwesome.Solid.Question; + icon.Colour = colours.Orange0; + break; + case DownloadState.NotDownloaded: text.Text = "no map"; icon.Icon = FontAwesome.Solid.MinusCircle; From fec086aec8d456eb7d6a71f4d29dcebe53f12516 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 15:42:53 +0900 Subject: [PATCH 0449/2100] Fix `OnlinePlayBeatmapAvailabilityTracker` not passing through `Unknown` state --- osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 29f75bed97..331a471ad5 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -124,6 +124,9 @@ namespace osu.Game.Online.Rooms switch (downloadTracker.State.Value) { case DownloadState.Unknown: + availability.Value = BeatmapAvailability.Unknown(); + break; + case DownloadState.NotDownloaded: availability.Value = BeatmapAvailability.NotDownloaded(); break; From 09bc8e45de43b44a4b0e235a99c8f69fac7624bc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 28 Jun 2023 15:04:13 +0900 Subject: [PATCH 0450/2100] Refactoring --- .../Difficulty/CatchDifficultyCalculator.cs | 2 +- ...cessor.cs => CatchLegacyScoreProcessor.cs} | 12 +-- .../Difficulty/ManiaDifficultyCalculator.cs | 2 +- ...cessor.cs => ManiaLegacyScoreProcessor.cs} | 2 +- .../Difficulty/OsuDifficultyCalculator.cs | 2 +- ...rocessor.cs => OsuLegacyScoreProcessor.cs} | 12 +-- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- .../Difficulty/TaikoDifficultyCalculator.cs | 2 +- ...cessor.cs => TaikoLegacyScoreProcessor.cs} | 12 +-- osu.Game/BackgroundBeatmapProcessor.cs | 8 +- osu.Game/Database/RealmAccess.cs | 13 ++- .../StandardisedScoreMigrationTools.cs | 87 +++++++++++++++++++ osu.Game/OsuGameBase.cs | 2 +- .../Rulesets/Scoring/ILegacyScoreProcessor.cs | 7 ++ osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 2 +- osu.Game/Scoring/ScoreImporter.cs | 61 +------------ osu.Game/Scoring/ScoreInfo.cs | 11 +++ osu.Game/Scoring/ScoreManager.cs | 2 - 18 files changed, 133 insertions(+), 108 deletions(-) rename osu.Game.Rulesets.Catch/Difficulty/{CatchScoreV1Processor.cs => CatchLegacyScoreProcessor.cs} (88%) rename osu.Game.Rulesets.Mania/Difficulty/{ManiaScoreV1Processor.cs => ManiaLegacyScoreProcessor.cs} (93%) rename osu.Game.Rulesets.Osu/Difficulty/{OsuScoreV1Processor.cs => OsuLegacyScoreProcessor.cs} (91%) rename osu.Game.Rulesets.Taiko/Difficulty/{TaikoScoreV1Processor.cs => TaikoLegacyScoreProcessor.cs} (92%) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 5e562237c8..446a76486b 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty if (ComputeLegacyScoringValues) { - CatchScoreV1Processor sv1Processor = new CatchScoreV1Processor(); + CatchLegacyScoreProcessor sv1Processor = new CatchLegacyScoreProcessor(); sv1Processor.Simulate(workingBeatmap, beatmap, mods); attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; attributes.LegacyComboScore = sv1Processor.ComboScore; diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreProcessor.cs similarity index 88% rename from osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs rename to osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreProcessor.cs index b4cca610c3..67a813300d 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreProcessor.cs @@ -14,22 +14,12 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Difficulty { - internal class CatchScoreV1Processor : ILegacyScoreProcessor + internal class CatchLegacyScoreProcessor : ILegacyScoreProcessor { - /// - /// The accuracy portion of the legacy (ScoreV1) total score. - /// public int AccuracyScore { get; private set; } - /// - /// The combo-multiplied portion of the legacy (ScoreV1) total score. - /// public int ComboScore { get; private set; } - /// - /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. - /// This is made up of all judgements that would be or . - /// public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore; private int legacyBonusScore; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 5403c1f860..e94e9b667d 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty if (ComputeLegacyScoringValues) { - ManiaScoreV1Processor sv1Processor = new ManiaScoreV1Processor(); + ManiaLegacyScoreProcessor sv1Processor = new ManiaLegacyScoreProcessor(); sv1Processor.Simulate(workingBeatmap, beatmap, mods); attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; attributes.LegacyComboScore = sv1Processor.ComboScore; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreProcessor.cs similarity index 93% rename from osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs rename to osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreProcessor.cs index 9134ca4e2a..e30d06c7b0 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreProcessor.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Difficulty { - internal class ManiaScoreV1Processor : ILegacyScoreProcessor + internal class ManiaLegacyScoreProcessor : ILegacyScoreProcessor { public int AccuracyScore => 0; public int ComboScore { get; private set; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 7ecbb48ae6..e28dbd96ac 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (ComputeLegacyScoringValues) { - OsuScoreV1Processor sv1Processor = new OsuScoreV1Processor(); + OsuLegacyScoreProcessor sv1Processor = new OsuLegacyScoreProcessor(); sv1Processor.Simulate(workingBeatmap, beatmap, mods); attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; attributes.LegacyComboScore = sv1Processor.ComboScore; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreProcessor.cs similarity index 91% rename from osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs rename to osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreProcessor.cs index 2e40d03fc0..a5e12e5564 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreProcessor.cs @@ -14,22 +14,12 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Difficulty { - internal class OsuScoreV1Processor : ILegacyScoreProcessor + internal class OsuLegacyScoreProcessor : ILegacyScoreProcessor { - /// - /// The accuracy portion of the legacy (ScoreV1) total score. - /// public int AccuracyScore { get; private set; } - /// - /// The combo-multiplied portion of the legacy (ScoreV1) total score. - /// public int ComboScore { get; private set; } - /// - /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. - /// This is made up of all judgements that would be or . - /// public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore; private int legacyBonusScore; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index c82f10c017..9b094ea1b1 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -323,6 +323,6 @@ namespace osu.Game.Rulesets.Osu public override RulesetSetupSection CreateEditorSetupSection() => new OsuSetupSection(); - public override ILegacyScoreProcessor CreateLegacyScoreProcessor() => new OsuScoreV1Processor(); + public override ILegacyScoreProcessor CreateLegacyScoreProcessor() => new OsuLegacyScoreProcessor(); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index b7f82b7512..28268d9a13 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (ComputeLegacyScoringValues) { - TaikoScoreV1Processor sv1Processor = new TaikoScoreV1Processor(); + TaikoLegacyScoreProcessor sv1Processor = new TaikoLegacyScoreProcessor(); sv1Processor.Simulate(workingBeatmap, beatmap, mods); attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; attributes.LegacyComboScore = sv1Processor.ComboScore; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreProcessor.cs similarity index 92% rename from osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs rename to osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreProcessor.cs index eaa82e695e..c9f508f5e9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreProcessor.cs @@ -14,22 +14,12 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty { - internal class TaikoScoreV1Processor : ILegacyScoreProcessor + internal class TaikoLegacyScoreProcessor : ILegacyScoreProcessor { - /// - /// The accuracy portion of the legacy (ScoreV1) total score. - /// public int AccuracyScore { get; private set; } - /// - /// The combo-multiplied portion of the legacy (ScoreV1) total score. - /// public int ComboScore { get; private set; } - /// - /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. - /// This is made up of all judgements that would be or . - /// public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore; private int legacyBonusScore; diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index c49edec87d..44aceac1ca 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -18,6 +18,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; using osu.Game.Screens.Play; namespace osu.Game @@ -27,6 +28,9 @@ namespace osu.Game [Resolved] private RulesetStore rulesetStore { get; set; } = null!; + [Resolved] + private BeatmapManager beatmapManager { get; set; } = null!; + [Resolved] private ScoreManager scoreManager { get; set; } = null!; @@ -241,7 +245,7 @@ namespace osu.Game try { var score = scoreManager.Query(s => s.ID == id); - long newTotalScore = scoreManager.ConvertFromLegacyTotalScore(score); + long newTotalScore = StandardisedScoreMigrationTools.ConvertFromLegacyTotalScore(score, beatmapManager); // Can't use async overload because we're not on the update thread. // ReSharper disable once MethodHasAsyncOverload @@ -249,7 +253,7 @@ namespace osu.Game { ScoreInfo s = r.Find(id); s.TotalScore = newTotalScore; - s.Version = 30000003; + s.Version = LegacyScoreEncoder.LATEST_VERSION; }); Logger.Log($"Converted total score for score {id}"); diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 727ddf06d7..93d70d7aea 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -78,7 +78,7 @@ namespace osu.Game.Database /// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files. /// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes. /// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations. - /// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and move TotalScore into LegacyTotalScore for legacy scores. + /// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores. /// private const int schema_version = 31; @@ -974,8 +974,15 @@ namespace osu.Game.Database foreach (var score in scores) { - score.LegacyTotalScore = score.TotalScore; - score.Version = 30000002; // Last version before legacy total score conversion. + if (score.IsLegacyScore) + { + score.LegacyTotalScore = score.TotalScore; + + // Scores with this version will trigger the update process in BackgroundBeatmapProcessor. + score.Version = 30000002; + } + else + score.Version = LegacyScoreEncoder.LATEST_VERSION; } break; diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 582a656efa..98e8671ede 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; @@ -185,6 +186,92 @@ namespace osu.Game.Database return (long)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier); } + /// + /// Converts from to the new standardised scoring of . + /// + /// The score to convert the total score of. + /// A used for lookups. + /// The standardised total score. + public static long ConvertFromLegacyTotalScore(ScoreInfo score, BeatmapManager beatmaps) + { + if (!score.IsLegacyScore) + return score.TotalScore; + + var beatmap = beatmaps.GetWorkingBeatmap(score.BeatmapInfo); + var ruleset = score.Ruleset.CreateInstance(); + + var sv1Processor = ruleset.CreateLegacyScoreProcessor(); + if (sv1Processor == null) + return score.TotalScore; + + sv1Processor.Simulate(beatmap, beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods), score.Mods); + + return ConvertFromLegacyTotalScore(score, new DifficultyAttributes + { + LegacyAccuracyScore = sv1Processor.AccuracyScore, + LegacyComboScore = sv1Processor.ComboScore, + LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio + }); + } + + /// + /// Converts from to the new standardised scoring of . + /// + /// The score to convert the total score of. + /// Difficulty attributes providing the legacy scoring values + /// (, , and ) + /// for the beatmap which the score was set on. + /// The standardised total score. + public static long ConvertFromLegacyTotalScore(ScoreInfo score, DifficultyAttributes attributes) + { + if (!score.IsLegacyScore) + return score.TotalScore; + + int maximumLegacyAccuracyScore = attributes.LegacyAccuracyScore; + int maximumLegacyComboScore = attributes.LegacyComboScore; + double maximumLegacyBonusRatio = attributes.LegacyBonusScoreRatio; + double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n); + + // The part of total score that doesn't include bonus. + int maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore; + + // The combo proportion is calculated as a proportion of maximumLegacyBaseScore. + double comboProportion = Math.Min(1, (double)score.LegacyTotalScore / maximumLegacyBaseScore); + + // The bonus proportion makes up the rest of the score that exceeds maximumLegacyBaseScore. + double bonusProportion = Math.Max(0, (score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio); + + switch (score.Ruleset.OnlineID) + { + case 0: + return (long)Math.Round(( + 700000 * comboProportion + + 300000 * Math.Pow(score.Accuracy, 10) + + bonusProportion) * modMultiplier); + + case 1: + return (long)Math.Round(( + 250000 * comboProportion + + 750000 * Math.Pow(score.Accuracy, 3.6) + + bonusProportion) * modMultiplier); + + case 2: + return (long)Math.Round(( + 600000 * comboProportion + + 400000 * score.Accuracy + + bonusProportion) * modMultiplier); + + case 3: + return (long)Math.Round(( + 990000 * comboProportion + + 10000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy) + + bonusProportion) * modMultiplier); + + default: + return score.TotalScore; + } + } + private class FakeHit : HitObject { private readonly Judgement judgement; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 6737caa5f9..cdd3b368bd 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -99,7 +99,7 @@ namespace osu.Game /// private const double global_track_volume_adjust = 0.8; - public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild; + public virtual bool UseDevelopmentServer => false; public virtual EndpointConfiguration CreateEndpoints() => UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ExperimentalEndpointConfiguration(); diff --git a/osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs index 70234a9b17..c689d3610d 100644 --- a/osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs @@ -25,6 +25,13 @@ namespace osu.Game.Rulesets.Scoring /// double BonusScoreRatio { get; } + /// + /// Performs the simulation, computing the maximum , , + /// and achievable for the given beatmap. + /// + /// The working beatmap. + /// A playable version of the beatmap for the ruleset. + /// The applied mods. void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods); } } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 6c8b99b842..a5ac151cf8 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -29,7 +29,7 @@ namespace osu.Game.Scoring.Legacy /// /// 30000001: Appends to the end of scores. /// 30000002: Score stored to replay calculated using the Score V2 algorithm. - /// 30000003: First version after legacy total score migration. + /// 30000003: First version after converting legacy total score to standardised. /// /// public const int LATEST_VERSION = 30000003; diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index e8f23fdc10..eb57f9a560 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -89,7 +89,7 @@ namespace osu.Game.Scoring if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model)) model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model); else if (model.IsLegacyScore) - model.TotalScore = ConvertFromLegacyTotalScore(model); + model.TotalScore = StandardisedScoreMigrationTools.ConvertFromLegacyTotalScore(model, beatmaps()); } /// @@ -153,65 +153,6 @@ namespace osu.Game.Scoring #pragma warning restore CS0618 } - public long ConvertFromLegacyTotalScore(ScoreInfo score) - { - if (!score.IsLegacyScore) - return score.TotalScore; - - var beatmap = beatmaps().GetWorkingBeatmap(score.BeatmapInfo); - var ruleset = score.Ruleset.CreateInstance(); - - var sv1Processor = ruleset.CreateLegacyScoreProcessor(); - if (sv1Processor == null) - return score.TotalScore; - - sv1Processor.Simulate(beatmap, beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods), score.Mods); - - int maximumLegacyAccuracyScore = sv1Processor.AccuracyScore; - int maximumLegacyComboScore = sv1Processor.ComboScore; - double maximumLegacyBonusRatio = sv1Processor.BonusScoreRatio; - double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n); - - // The part of total score that doesn't include bonus. - int maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore; - - // The combo proportion is calculated as a proportion of maximumLegacyBaseScore. - double comboProportion = Math.Min(1, (double)score.LegacyTotalScore / maximumLegacyBaseScore); - - // The bonus proportion makes up the rest of the score that exceeds maximumLegacyBaseScore. - double bonusProportion = Math.Max(0, (score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio); - - switch (ruleset.RulesetInfo.OnlineID) - { - case 0: - return (long)Math.Round(( - 700000 * comboProportion - + 300000 * Math.Pow(score.Accuracy, 10) - + bonusProportion) * modMultiplier); - - case 1: - return (long)Math.Round(( - 250000 * comboProportion - + 750000 * Math.Pow(score.Accuracy, 3.6) - + bonusProportion) * modMultiplier); - - case 2: - return (long)Math.Round(( - 600000 * comboProportion - + 400000 * score.Accuracy - + bonusProportion) * modMultiplier); - - case 3: - return (long)Math.Round(( - 990000 * comboProportion - + 10000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy) - + bonusProportion) * modMultiplier); - - default: - return score.TotalScore; - } - } - // Very naive local caching to improve performance of large score imports (where the username is usually the same for most or all scores). private readonly Dictionary usernameLookupCache = new Dictionary(); diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index a0a0799fb1..99b91318fd 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -57,6 +57,9 @@ namespace osu.Game.Scoring /// /// Used to preserve the total score for legacy scores. /// + /// + /// Not populated if is false. + /// public long LegacyTotalScore { get; set; } public int MaxCombo { get; set; } @@ -69,6 +72,14 @@ namespace osu.Game.Scoring public double? PP { get; set; } + /// + /// The version of this score as stored in the database. + /// If this does not match , + /// then the score has not yet been updated to reflect the current scoring values. + /// + /// + /// This may not match the version stored in the replay files. + /// public int Version { get; set; } = LegacyScoreEncoder.LATEST_VERSION; [Indexed] diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index fd5e9c851c..55bcb9f79d 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -169,8 +169,6 @@ namespace osu.Game.Scoring /// The score to populate the statistics of. public void PopulateMaximumStatistics(ScoreInfo score) => scoreImporter.PopulateMaximumStatistics(score); - public long ConvertFromLegacyTotalScore(ScoreInfo score) => scoreImporter.ConvertFromLegacyTotalScore(score); - #region Implementation of IPresentImports public Action>> PresentImport From af25ffbe8122587e437aeac0e42084412296d09c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 28 Jun 2023 16:14:44 +0900 Subject: [PATCH 0451/2100] Remove JSON output --- osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 48e67ff425..5a01faa417 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -39,32 +39,29 @@ namespace osu.Game.Rulesets.Difficulty /// /// The combined star rating of all skills. /// - [JsonProperty("star_rating", Order = -7)] + [JsonProperty("star_rating", Order = -3)] public double StarRating { get; set; } /// /// The maximum achievable combo. /// - [JsonProperty("max_combo", Order = -6)] + [JsonProperty("max_combo", Order = -2)] public int MaxCombo { get; set; } /// /// The accuracy portion of the legacy (ScoreV1) total score. /// - [JsonProperty("legacy_accuracy_score", Order = -5)] public int LegacyAccuracyScore { get; set; } /// /// The combo-multiplied portion of the legacy (ScoreV1) total score. /// - [JsonProperty("legacy_combo_score", Order = -4)] public int LegacyComboScore { get; set; } /// /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. /// This is made up of all judgements that would be or . /// - [JsonProperty("legacy_bonus_score_ratio", Order = -3)] public double LegacyBonusScoreRatio { get; set; } /// From 1ca4e39fc33f090046bc0aa2fffe191d0bc24807 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 28 Jun 2023 16:30:50 +0900 Subject: [PATCH 0452/2100] Allow legacy scores to be displayed in "classic" scoring mode --- osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs index e298d51ccb..980b742585 100644 --- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs @@ -14,13 +14,7 @@ namespace osu.Game.Scoring.Legacy => getDisplayScore(scoreProcessor.Ruleset.RulesetInfo.OnlineID, scoreProcessor.TotalScore.Value, mode, scoreProcessor.MaximumStatistics); public static long GetDisplayScore(this ScoreInfo scoreInfo, ScoringMode mode) - { - // Temporary to not scale stable scores that are already in the XX-millions with the classic scoring mode. - if (scoreInfo.IsLegacyScore) - return scoreInfo.TotalScore; - - return getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics); - } + => getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics); private static long getDisplayScore(int rulesetId, long score, ScoringMode mode, IReadOnlyDictionary maximumStatistics) { From 5d209b3ffc2d1bfd6d3274616f5f2a5b4d4d7371 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 16:38:20 +0900 Subject: [PATCH 0453/2100] Change default availability in `MultiplayerRoomUser` to `Unknown` --- osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index d70a2797c4..f769b4c805 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -29,7 +29,7 @@ namespace osu.Game.Online.Multiplayer /// The availability state of the current beatmap. /// [Key(2)] - public BeatmapAvailability BeatmapAvailability { get; set; } = BeatmapAvailability.LocallyAvailable(); + public BeatmapAvailability BeatmapAvailability { get; set; } = BeatmapAvailability.Unknown(); /// /// Any mods applicable only to the local user. From 6ce0ca832e57f2b9d4e96d9a3b9a908d49896f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 28 Jun 2023 19:52:08 +0200 Subject: [PATCH 0454/2100] Delete test case covering beatmap detach scenario Due to being fundamentally incompatible with the `Debug.Fail()` call added in 99e55bb9c03972fdee0569ee4ae7a5c37eac84a9. This reverts commit 4ecc724841ac075f24f8d5695fb277672ccceca8. --- .../Visual/Editing/TestSceneEditorSaving.cs | 31 ++----------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index 7191ae6a57..64c48e74cf 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable disable + using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -94,33 +96,6 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("Beatmap still has correct timeline zoom", () => EditorBeatmap.BeatmapInfo.TimelineZoom == changedTimelineZoom); } - [Test] - public void TestSaveWithDetachedBeatmap() - { - AddStep("Set overall difficulty", () => EditorBeatmap.Difficulty.OverallDifficulty = 7); - - SaveEditor(); - - AddStep("Detach beatmap from set", () => - { - Realm.Write(r => - { - BeatmapSetInfo? beatmapSet = r.Find(EditorBeatmap.BeatmapInfo.BeatmapSet!.ID); - BeatmapInfo? beatmap = r.Find(EditorBeatmap.BeatmapInfo.ID); - - beatmapSet.Beatmaps.Remove(beatmap); - }); - }); - - SaveEditor(); - - AddAssert("Beatmap has correct overall difficulty", () => EditorBeatmap.Difficulty.OverallDifficulty == 7); - - ReloadEditorToSameBeatmap(); - - AddAssert("Beatmap still has correct overall difficulty", () => EditorBeatmap.Difficulty.OverallDifficulty == 7); - } - [Test] public void TestDifficulty() { @@ -155,7 +130,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestLengthAndStarRatingUpdated() { - WorkingBeatmap working = null!; + WorkingBeatmap working = null; double lastStarRating = 0; double lastLength = 0; From 0940ab1e11cb246975908f8f00974511f1864fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 28 Jun 2023 20:47:00 +0200 Subject: [PATCH 0455/2100] Add failing tests covering correct flip handling --- .../Editing/TestSceneComposerSelection.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index 4d99c47f77..d6934a3770 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -133,6 +133,32 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("objects reverted to original position", () => addedObjects[0].Position == new Vector2(0)); } + [Test] + public void TestGlobalFlipHotkeys() + { + HitCircle addedObject = null; + + AddStep("add hitobjects", () => EditorBeatmap.Add(addedObject = new HitCircle { StartTime = 100 })); + + AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); + + AddStep("flip horizontally across playfield", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.H); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddAssert("objects flipped horizontally", () => addedObject.Position == new Vector2(OsuPlayfield.BASE_SIZE.X, 0)); + + AddStep("flip vertically across playfield", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.J); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddAssert("objects flipped vertically", () => addedObject.Position == OsuPlayfield.BASE_SIZE); + } + [Test] public void TestBasicSelect() { From e4e08c0f5fb787091d28a38b4e67bc59b2b0b846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 28 Jun 2023 20:48:22 +0200 Subject: [PATCH 0456/2100] Fix selection handlers eating hotkey presses they didn't handle --- .../Edit/Compose/Components/SelectionHandler.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 9ec59cf833..052cb18a5d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -160,21 +160,23 @@ namespace osu.Game.Screens.Edit.Compose.Components if (e.Repeat) return false; + bool handled; + switch (e.Action) { case GlobalAction.EditorFlipHorizontally: ChangeHandler?.BeginChange(); - HandleFlip(Direction.Horizontal, true); + handled = HandleFlip(Direction.Horizontal, true); ChangeHandler?.EndChange(); - return true; + return handled; case GlobalAction.EditorFlipVertically: ChangeHandler?.BeginChange(); - HandleFlip(Direction.Vertical, true); + handled = HandleFlip(Direction.Vertical, true); ChangeHandler?.EndChange(); - return true; + return handled; } return false; From ea8700053917628b69aa37a64d508753cac63f11 Mon Sep 17 00:00:00 2001 From: Bastian Pedersen Date: Wed, 28 Jun 2023 21:11:56 +0200 Subject: [PATCH 0457/2100] Localise chat related notifications --- osu.Game/Localisation/NotificationsStrings.cs | 10 ++++++++++ osu.Game/Online/Chat/MessageNotifier.cs | 5 +++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index b6f2a55e37..44e440e8d9 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -88,6 +88,16 @@ Please try changing your audio device to a working setting."); /// public static LocalisableString LinkTypeNotSupported => new TranslatableString(getKey(@"unsupported_link_type"), @"This link type is not yet supported!"); + /// + /// "You received a private message from '{0}'. Click to read it!" + /// + public static LocalisableString PrivateMessageReceived(string username) => new TranslatableString(getKey(@"private_message_received"), @"You received a private message from '{0}'. Click to read it!", username); + + /// + /// "Your name was mentioned in chat by '{0}'. Click to find out why!" + /// + public static LocalisableString YourNameWasMentioned(string username) => new TranslatableString(getKey(@"your_name_was_mentioned"), @"Your name was mentioned in chat by '{0}'. Click to find out why!", username); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 52bdd36169..ae249d1b7f 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; @@ -154,7 +155,7 @@ namespace osu.Game.Online.Chat : base(message, channel) { Icon = FontAwesome.Solid.Envelope; - Text = $"You received a private message from '{message.Sender.Username}'. Click to read it!"; + Text = NotificationsStrings.PrivateMessageReceived(message.Sender.Username); } } @@ -164,7 +165,7 @@ namespace osu.Game.Online.Chat : base(message, channel) { Icon = FontAwesome.Solid.At; - Text = $"Your name was mentioned in chat by '{message.Sender.Username}'. Click to find out why!"; + Text = NotificationsStrings.YourNameWasMentioned(message.Sender.Username); } } From 537404440d427dad03c1d784023ee4871b93a77c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Jun 2023 13:17:42 +0900 Subject: [PATCH 0458/2100] Set the beatmap locally available for participants list tests --- .../Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index a01d2bf9fc..95ae4c5e80 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -383,6 +383,8 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddUntilStep("wait for list to load", () => participantsList?.IsLoaded == true); + + AddStep("set beatmap available", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable())); } private void checkProgressBarVisibility(bool visible) => From 34f53965c4fe2cc4c577a3c88aeb8b54f6c94644 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Jun 2023 13:55:03 +0900 Subject: [PATCH 0459/2100] Never remove significant digits from stsar rating displays Closes https://github.com/ppy/osu/issues/24079. --- osu.Game.Tournament/Components/SongBar.cs | 2 +- osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index aeceece160..4f4a02ccf1 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -188,7 +188,7 @@ namespace osu.Game.Tournament.Components Children = new Drawable[] { new DiffPiece(stats), - new DiffPiece(("Star Rating", $"{beatmap.StarRating:0.##}{srExtra}")) + new DiffPiece(("Star Rating", $"{beatmap.StarRating:0.00}{srExtra}")) } }, new FillFlowContainer diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index 104f861df7..1f38e2ed6c 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -185,7 +185,7 @@ namespace osu.Game.Overlays.BeatmapSet OnHovered = beatmap => { showBeatmap(beatmap); - starRating.Text = beatmap.StarRating.ToLocalisableString(@"0.##"); + starRating.Text = beatmap.StarRating.ToLocalisableString(@"0.00"); starRatingContainer.FadeIn(100); }, OnClicked = beatmap => { Beatmap.Value = beatmap; }, From 351f217c8c54f96df61aebafb1ae7273087cc89c Mon Sep 17 00:00:00 2001 From: Cootz Date: Thu, 29 Jun 2023 08:07:43 +0300 Subject: [PATCH 0460/2100] Reassign existing scores to new/re-exported beatmap --- osu.Game/Beatmaps/BeatmapImporter.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 7d367ef77d..04c00ed98a 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -20,6 +20,7 @@ using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; +using osu.Game.Scoring; using Realms; namespace osu.Game.Beatmaps @@ -199,6 +200,16 @@ namespace osu.Game.Beatmaps LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be disassociated and marked for deletion."); } } + + //Because of specific score storing in Osu! database can already contain scores for imported beatmap. + //To restore scores we need to manually reassign them to new/re-exported beatmap. + foreach (BeatmapInfo beatmap in beatmapSet.Beatmaps) + { + IQueryable scores = realm.All().Where(score => score.BeatmapHash == beatmap.Hash); + + if (scores.Any()) + scores.ForEach(score => score.BeatmapInfo = beatmap); //We intentionally ignore BeatmapHash because we checked hash equality + } } protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters) From 47ccbddfb1d0d3a583a649dd531a6dfd63120ed2 Mon Sep 17 00:00:00 2001 From: Cootz Date: Thu, 29 Jun 2023 08:08:10 +0300 Subject: [PATCH 0461/2100] Reword comment --- osu.Game/Beatmaps/BeatmapImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 04c00ed98a..bf549f40c4 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -201,7 +201,7 @@ namespace osu.Game.Beatmaps } } - //Because of specific score storing in Osu! database can already contain scores for imported beatmap. + //Because of specific score storing in Osu! database, it can already contain scores for imported beatmap. //To restore scores we need to manually reassign them to new/re-exported beatmap. foreach (BeatmapInfo beatmap in beatmapSet.Beatmaps) { From 829044de59deed0445ed4670838fbd76cebb2508 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 29 Jun 2023 17:15:48 +0900 Subject: [PATCH 0462/2100] Revert unintented change --- osu.Game/OsuGameBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index cdd3b368bd..6737caa5f9 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -99,7 +99,7 @@ namespace osu.Game /// private const double global_track_volume_adjust = 0.8; - public virtual bool UseDevelopmentServer => false; + public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild; public virtual EndpointConfiguration CreateEndpoints() => UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ExperimentalEndpointConfiguration(); From c8162814945f48dcc233a45a6738678a1e4c688c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 29 Jun 2023 17:16:33 +0900 Subject: [PATCH 0463/2100] Make BackgroundBeatmapProcessor task long-running --- osu.Game/BackgroundBeatmapProcessor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 44aceac1ca..0b49bb26b2 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -55,14 +55,14 @@ namespace osu.Game { base.LoadComplete(); - Task.Run(() => + Task.Factory.StartNew(() => { Logger.Log("Beginning background beatmap processing.."); checkForOutdatedStarRatings(); processBeatmapSetsWithMissingMetrics(); processScoresWithMissingStatistics(); convertLegacyTotalScoreToStandardised(); - }).ContinueWith(t => + }, TaskCreationOptions.LongRunning).ContinueWith(t => { if (t.Exception?.InnerException is ObjectDisposedException) { From ddd870e843a26263e82bc5696ee1259fa47c2415 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 29 Jun 2023 17:19:10 +0900 Subject: [PATCH 0464/2100] Make LegacyTotalScore nullable --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 5 ++++- osu.Game/Scoring/ScoreInfo.cs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 98e8671ede..c736c7e20e 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; @@ -227,6 +228,8 @@ namespace osu.Game.Database if (!score.IsLegacyScore) return score.TotalScore; + Debug.Assert(score.LegacyTotalScore != null); + int maximumLegacyAccuracyScore = attributes.LegacyAccuracyScore; int maximumLegacyComboScore = attributes.LegacyComboScore; double maximumLegacyBonusRatio = attributes.LegacyBonusScoreRatio; @@ -239,7 +242,7 @@ namespace osu.Game.Database double comboProportion = Math.Min(1, (double)score.LegacyTotalScore / maximumLegacyBaseScore); // The bonus proportion makes up the rest of the score that exceeds maximumLegacyBaseScore. - double bonusProportion = Math.Max(0, (score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio); + double bonusProportion = Math.Max(0, ((long)score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio); switch (score.Ruleset.OnlineID) { diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 99b91318fd..bdba81c685 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -60,7 +60,7 @@ namespace osu.Game.Scoring /// /// Not populated if is false. /// - public long LegacyTotalScore { get; set; } + public long? LegacyTotalScore { get; set; } public int MaxCombo { get; set; } From 6822871dab4dd6fc45667946c3308194dfcee8ec Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 29 Jun 2023 17:21:24 +0900 Subject: [PATCH 0465/2100] Move population of LegacyTotalScore to ScoreImporter --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 3 --- osu.Game/Scoring/ScoreImporter.cs | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index bf592d5988..c6461840aa 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -123,9 +123,6 @@ namespace osu.Game.Scoring.Legacy PopulateAccuracy(score.ScoreInfo); - if (score.ScoreInfo.IsLegacyScore) - score.ScoreInfo.LegacyTotalScore = score.ScoreInfo.TotalScore; - // before returning for database import, we must restore the database-sourced BeatmapInfo. // if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception. score.ScoreInfo.BeatmapInfo = workingBeatmap.BeatmapInfo; diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index eb57f9a560..5ada2a410d 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -89,7 +89,10 @@ namespace osu.Game.Scoring if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model)) model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model); else if (model.IsLegacyScore) + { + model.LegacyTotalScore = model.TotalScore; model.TotalScore = StandardisedScoreMigrationTools.ConvertFromLegacyTotalScore(model, beatmaps()); + } } /// From c6ad184d94ae15a26a16825fd7620498ec133935 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 29 Jun 2023 17:24:37 +0900 Subject: [PATCH 0466/2100] Move Ruleset method to ILegacyRuleset interface --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 ++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 ++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 4 ++-- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 ++ osu.Game/Database/StandardisedScoreMigrationTools.cs | 6 +++++- osu.Game/Rulesets/ILegacyRuleset.cs | 4 ++++ osu.Game/Rulesets/Ruleset.cs | 2 -- 7 files changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 8a0b8250d5..9862b7d886 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -202,6 +202,8 @@ namespace osu.Game.Rulesets.Catch public int LegacyID => 2; + public ILegacyScoreProcessor CreateLegacyScoreProcessor() => new CatchLegacyScoreProcessor(); + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame(); public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this); diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index e8fda3ec80..77cc3e06d2 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -302,6 +302,8 @@ namespace osu.Game.Rulesets.Mania public int LegacyID => 3; + public ILegacyScoreProcessor CreateLegacyScoreProcessor() => new ManiaLegacyScoreProcessor(); + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame(); public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new ManiaRulesetConfigManager(settings, RulesetInfo); diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 9b094ea1b1..abbd4a43c8 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -253,6 +253,8 @@ namespace osu.Game.Rulesets.Osu public int LegacyID => 0; + public ILegacyScoreProcessor CreateLegacyScoreProcessor() => new OsuLegacyScoreProcessor(); + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame(); public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new OsuRulesetConfigManager(settings, RulesetInfo); @@ -322,7 +324,5 @@ namespace osu.Game.Rulesets.Osu } public override RulesetSetupSection CreateEditorSetupSection() => new OsuSetupSection(); - - public override ILegacyScoreProcessor CreateLegacyScoreProcessor() => new OsuLegacyScoreProcessor(); } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index d6824109b3..af02c94d38 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -197,6 +197,8 @@ namespace osu.Game.Rulesets.Taiko public int LegacyID => 1; + public ILegacyScoreProcessor CreateLegacyScoreProcessor() => new TaikoLegacyScoreProcessor(); + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TaikoRulesetConfigManager(settings, RulesetInfo); diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index c736c7e20e..89bb908b1f 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -201,7 +202,10 @@ namespace osu.Game.Database var beatmap = beatmaps.GetWorkingBeatmap(score.BeatmapInfo); var ruleset = score.Ruleset.CreateInstance(); - var sv1Processor = ruleset.CreateLegacyScoreProcessor(); + if (ruleset is not ILegacyRuleset legacyRuleset) + return score.TotalScore; + + var sv1Processor = legacyRuleset.CreateLegacyScoreProcessor(); if (sv1Processor == null) return score.TotalScore; diff --git a/osu.Game/Rulesets/ILegacyRuleset.cs b/osu.Game/Rulesets/ILegacyRuleset.cs index f4b03baccd..ba12c1f559 100644 --- a/osu.Game/Rulesets/ILegacyRuleset.cs +++ b/osu.Game/Rulesets/ILegacyRuleset.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Scoring; + namespace osu.Game.Rulesets { public interface ILegacyRuleset @@ -11,5 +13,7 @@ namespace osu.Game.Rulesets /// Identifies the server-side ID of a legacy ruleset. /// int LegacyID { get; } + + ILegacyScoreProcessor CreateLegacyScoreProcessor(); } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 5501a3a7c5..490ec1475c 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -380,7 +380,5 @@ namespace osu.Game.Rulesets /// Can be overridden to add a ruleset-specific section to the editor beatmap setup screen. /// public virtual RulesetSetupSection? CreateEditorSetupSection() => null; - - public virtual ILegacyScoreProcessor? CreateLegacyScoreProcessor() => null; } } From 426f11b824e770a901741dcda2385c719198eae3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 29 Jun 2023 17:28:06 +0900 Subject: [PATCH 0467/2100] Apply a few other code reviews --- .../Difficulty/ManiaDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs | 2 +- osu.Game/BackgroundBeatmapProcessor.cs | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index e94e9b667d..d7994e6a0c 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty public override int Version => 20220902; - private IWorkingBeatmap workingBeatmap; + private readonly IWorkingBeatmap workingBeatmap; public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs index 88af50d36b..0e10f75378 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Tests private void addFlyingHit(HitType hitType) { - var tick = new DrumRollTick(null) { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current }; + var tick = new DrumRollTick(new DrumRoll()) { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current }; DrawableDrumRollTick h; DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType }); diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 0b49bb26b2..11e6a4619b 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; @@ -214,7 +215,7 @@ namespace osu.Game { foreach (var score in r.All().Where(s => s.IsLegacyScore)) { - if (score.RulesetID is not (0 or 1 or 2 or 3)) + if (!score.Ruleset.IsLegacyRuleset()) continue; if (score.Version >= 30000003) From e2db6159d6436e4b341d1a76864b4bdf10ef4d17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 13:46:02 +0900 Subject: [PATCH 0468/2100] Ensure "tablet support disabled" notification is only shown once --- osu.Game/OsuGame.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index fe98a8e286..93dd97ea15 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1164,7 +1164,9 @@ namespace osu.Game private void forwardTabletLogsToNotifications() { const string tablet_prefix = @"[Tablet] "; + bool notifyOnWarning = true; + bool notifyOnError = true; Logger.NewEntry += entry => { @@ -1175,6 +1177,11 @@ namespace osu.Game if (entry.Level == LogLevel.Error) { + if (!notifyOnError) + return; + + notifyOnError = false; + Schedule(() => { Notifications.Post(new SimpleNotification @@ -1213,7 +1220,11 @@ namespace osu.Game Schedule(() => { ITabletHandler tablet = Host.AvailableInputHandlers.OfType().SingleOrDefault(); - tablet?.Tablet.BindValueChanged(_ => notifyOnWarning = true, true); + tablet?.Tablet.BindValueChanged(_ => + { + notifyOnWarning = true; + notifyOnError = true; + }, true); }); } From e87cf6d2567da36e72e7f983e216daed68fd78db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 15:06:32 +0900 Subject: [PATCH 0469/2100] Move all remaining osu!taiko sample playback logic out of `DrawableHitObject`s --- .../Objects/Drawables/DrawableHit.cs | 36 ------------------- .../Drawables/DrawableTaikoHitObject.cs | 4 +-- .../UI/DrumSampleTriggerSource.cs | 19 +++++++--- 3 files changed, 17 insertions(+), 42 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 62c8457c58..5b79151225 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -4,14 +4,12 @@ #nullable disable using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Events; -using osu.Game.Audio; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Skinning.Default; @@ -93,40 +91,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ? new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit) : new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit); - public override IEnumerable GetSamples() - { - // normal and claps are always handled by the drum (see DrumSampleMapping). - // in addition, whistles are excluded as they are an alternative rim marker. - - var samples = HitObject.Samples.Where(s => - s.Name != HitSampleInfo.HIT_NORMAL - && s.Name != HitSampleInfo.HIT_CLAP - && s.Name != HitSampleInfo.HIT_WHISTLE); - - if (HitObject.Type == HitType.Rim && HitObject.IsStrong) - { - // strong + rim always maps to whistle. - // TODO: this should really be in the legacy decoder, but can't be because legacy encoding parity would be broken. - // when we add a taiko editor, this is probably not going to play nice. - - var corrected = samples.ToList(); - - for (int i = 0; i < corrected.Count; i++) - { - var s = corrected[i]; - - if (s.Name != HitSampleInfo.HIT_FINISH) - continue; - - corrected[i] = s.With(HitSampleInfo.HIT_WHISTLE); - } - - return corrected; - } - - return samples; - } - protected override void CheckForResult(bool userTriggered, double timeOffset) { Debug.Assert(HitObject.HitWindows != null); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 1b5d641612..3f4694d71d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -119,8 +119,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool RemoveWhenNotAlive => false; } - // Most osu!taiko hitsounds are managed by the drum (see DrumSampleTriggerSource). - public override IEnumerable GetSamples() => Enumerable.Empty(); + // osu!taiko hitsounds are managed by the drum (see DrumSampleTriggerSource). + public sealed override IEnumerable GetSamples() => Enumerable.Empty(); } public abstract partial class DrawableTaikoHitObject : DrawableTaikoHitObject diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index 92f2b74568..c732cc000f 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; +using System.Collections.Generic; using osu.Game.Audio; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.UI; @@ -18,12 +18,23 @@ namespace osu.Game.Rulesets.Taiko.UI public void Play(HitType hitType) { - var hitSample = GetMostValidObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); + TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; - if (hitSample == null) + if (hitObject == null) return; - PlaySamples(new ISampleInfo[] { new HitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL, hitSample.Bank, volume: hitSample.Volume) }); + List samplesToPlay = new List + { + hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL) + }; + + // strong + rim always maps to whistle. + if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true) + { + samplesToPlay.Add(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH)); + } + + PlaySamples(samplesToPlay.ToArray()); } public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead"); From c98abf1723f6bcc32896adb6ac1e9864c1826299 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 15:38:17 +0900 Subject: [PATCH 0470/2100] More correctly handle `StrongNestedHitObject`s --- osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index c732cc000f..adf02d88ce 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.UI }; // strong + rim always maps to whistle. - if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true) + if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject) { samplesToPlay.Add(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH)); } From 32c0f13f79b1c6474e08d70721ef12deee088607 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 15:38:24 +0900 Subject: [PATCH 0471/2100] Update tests to match new expectations --- .../TestSceneDrumSampleTriggerSource.cs | 117 +++++++++--------- .../TestSceneSampleOutput.cs | 15 +-- 2 files changed, 67 insertions(+), 65 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs index bce855ae45..4133b96d42 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs @@ -72,13 +72,13 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); } [Test] @@ -100,13 +100,13 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); seekTo(200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); } [Test] @@ -145,23 +145,23 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first)); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); seekTo(120); AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first)); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); seekTo(480); AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second)); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); seekTo(700); AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second)); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); } [Test] @@ -174,8 +174,8 @@ namespace osu.Game.Rulesets.Taiko.Tests StartTime = 100, Samples = new List { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"), - new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM), + new HitSampleInfo(HitSampleInfo.HIT_FINISH, HitSampleInfo.BANK_DRUM) // implies strong } }; hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); @@ -184,13 +184,13 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); seekTo(200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); } [Test] @@ -213,18 +213,18 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(600); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); } [Test] @@ -247,18 +247,18 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); seekTo(600); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); seekTo(1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); } [Test] @@ -272,8 +272,8 @@ namespace osu.Game.Rulesets.Taiko.Tests EndTime = 1100, Samples = new List { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"), - new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM), + new HitSampleInfo(HitSampleInfo.HIT_FINISH, HitSampleInfo.BANK_DRUM) // implies strong } }; drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); @@ -282,18 +282,18 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); seekTo(600); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); seekTo(1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); } [Test] @@ -319,18 +319,18 @@ namespace osu.Game.Rulesets.Taiko.Tests // This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits. // But for sample playback purposes they can be ignored as noise. AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(600); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(1200); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); } [Test] @@ -344,7 +344,7 @@ namespace osu.Game.Rulesets.Taiko.Tests EndTime = 1100, Samples = new List { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum") + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM) } }; swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); @@ -356,25 +356,26 @@ namespace osu.Game.Rulesets.Taiko.Tests // This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits. // But for sample playback purposes they can be ignored as noise. AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); seekTo(600); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); seekTo(1200); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); } - private void checkSound(HitType hitType, string expectedName, string expectedBank) + private void checkSamples(HitType hitType, string expectedSamplesCsv, string expectedBank) { AddStep($"hit {hitType}", () => triggerSource.Play(hitType)); - AddAssert($"last played sample is {expectedName}", () => triggerSource.LastPlayedSamples!.OfType().Single().Name, () => Is.EqualTo(expectedName)); - AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType().Single().Bank, () => Is.EqualTo(expectedBank)); + AddAssert($"last played sample is {expectedSamplesCsv}", () => string.Join(',', triggerSource.LastPlayedSamples!.OfType().Select(s => s.Name)), + () => Is.EqualTo(expectedSamplesCsv)); + AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType().First().Bank, () => Is.EqualTo(expectedBank)); } private void seekTo(double time) => AddStep($"seek to {time}", () => gameplayClock.Seek(time)); diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs index 2429b71095..a548a14d88 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs @@ -3,15 +3,16 @@ using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Framework.Testing; -using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.UI; namespace osu.Game.Rulesets.Taiko.Tests { /// - /// Taiko has some interesting rules for legacy mappings. + /// Taiko doesn't output any samples. They are all handled externally by . /// [HeadlessTest] public partial class TestSceneSampleOutput : TestSceneTaikoPlayer @@ -26,10 +27,10 @@ namespace osu.Game.Rulesets.Taiko.Tests string.Empty, string.Empty, string.Empty, - HitSampleInfo.HIT_FINISH, - HitSampleInfo.HIT_WHISTLE, - HitSampleInfo.HIT_WHISTLE, - HitSampleInfo.HIT_WHISTLE, + string.Empty, + string.Empty, + string.Empty, + string.Empty, }; var actualSampleNames = new List(); @@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Taiko.Tests AddUntilStep("all samples collected", () => actualSampleNames.Count == expectedSampleNames.Length); - AddAssert("samples are correct", () => actualSampleNames.SequenceEqual(expectedSampleNames)); + AddAssert("samples are correct", () => actualSampleNames, () => Is.EqualTo(expectedSampleNames)); } protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TaikoBeatmapConversionTest().GetBeatmap("sample-to-type-conversions"); From 571dbf5ab8d58e4d5712f18da4c400ca6f7c3f8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 15:42:39 +0900 Subject: [PATCH 0472/2100] Adjust logic to avoid creating `List<>` each playback --- .../UI/DrumSampleTriggerSource.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index adf02d88ce..a04c4b60f2 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Game.Audio; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.UI; @@ -23,18 +22,20 @@ namespace osu.Game.Rulesets.Taiko.UI if (hitObject == null) return; - List samplesToPlay = new List - { - hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL) - }; + var baseSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); - // strong + rim always maps to whistle. if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject) { - samplesToPlay.Add(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH)); + PlaySamples(new ISampleInfo[] + { + hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH), + baseSample + }); + } + else + { + PlaySamples(new ISampleInfo[] { baseSample }); } - - PlaySamples(samplesToPlay.ToArray()); } public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead"); From 428383708c1ec2483ba2c39b8b6575aa28e09537 Mon Sep 17 00:00:00 2001 From: Cootz Date: Fri, 30 Jun 2023 20:13:14 +0300 Subject: [PATCH 0473/2100] Add test for import --- .../Database/BeatmapImporterTests.cs | 68 +++++++++++++++---- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 446eb72b04..53e5fec075 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -18,6 +18,7 @@ using osu.Game.Extensions; using osu.Game.Models; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; +using osu.Game.Scoring; using osu.Game.Tests.Resources; using Realms; using SharpCompress.Archives; @@ -416,6 +417,51 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestImport_ThenChangeMapWithScore_ThenImport() + { + RunTestWithRealmAsync(async (realm, storage) => + { + var importer = new BeatmapImporter(storage, realm); + + string? temp = TestResources.GetTestBeatmapForImport(); + + var imported = await LoadOszIntoStore(importer, realm.Realm); + + await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First()); + + //editor work imitation + realm.Run(r => + { + r.Write(() => + { + BeatmapInfo beatmap = imported.Beatmaps.First(); + beatmap.Hash = "new_hash"; + beatmap.ResetOnlineInfo(); + }); + }); + + Assert.That(imported.Beatmaps.First().Scores.Any()); + + var importedSecondTime = await importer.Import(new ImportTask(temp)); + + EnsureLoaded(realm.Realm); + + // check the newly "imported" beatmap is not the original. + Assert.NotNull(importedSecondTime); + Debug.Assert(importedSecondTime != null); + Assert.That(imported.ID != importedSecondTime.ID); + + var importedFirstTimeBeatmap = imported.Beatmaps.First(); + var importedSecondTimeBeatmap = importedSecondTime.PerformRead(s => s.Beatmaps.First()); + + Assert.That(importedFirstTimeBeatmap.ID != importedSecondTimeBeatmap.ID); + Assert.That(importedFirstTimeBeatmap.Hash != importedSecondTimeBeatmap.Hash); + Assert.That(!importedFirstTimeBeatmap.Scores.Any()); + Assert.That(importedSecondTimeBeatmap.Scores.Count() == 1); + }); + } + [Test] public void TestImportThenImportWithChangedFile() { @@ -1074,18 +1120,16 @@ namespace osu.Game.Tests.Database Assert.IsTrue(realm.All().First(_ => true).DeletePending); } - private static Task createScoreForBeatmap(Realm realm, BeatmapInfo beatmap) - { - // TODO: reimplement when we have score support in realm. - // return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo - // { - // OnlineID = 2, - // Beatmap = beatmap, - // BeatmapInfoID = beatmap.ID - // }, new ImportScoreTest.TestArchiveReader()); - - return Task.CompletedTask; - } + private static Task createScoreForBeatmap(Realm realm, BeatmapInfo beatmap) => + realm.WriteAsync(() => + { + realm.Add(new ScoreInfo + { + OnlineID = 2, + BeatmapInfo = beatmap, + BeatmapHash = beatmap.Hash + }); + }); private static void checkBeatmapSetCount(Realm realm, int expected, bool includeDeletePending = false) { From f5d3a2458272fe6de9b8024e56d250318df1b0d2 Mon Sep 17 00:00:00 2001 From: Cootz Date: Fri, 30 Jun 2023 20:15:38 +0300 Subject: [PATCH 0474/2100] Rename test --- osu.Game.Tests/Database/BeatmapImporterTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 53e5fec075..1f42979d11 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -418,7 +418,7 @@ namespace osu.Game.Tests.Database } [Test] - public void TestImport_ThenChangeMapWithScore_ThenImport() + public void TestImport_ThenModifyMapWithScore_ThenImport() { RunTestWithRealmAsync(async (realm, storage) => { From e505e71d07ae4109ad66de2e6fee801bd217cc16 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 30 Jun 2023 21:40:00 +0200 Subject: [PATCH 0475/2100] Merge the two `app.manifest` files --- app.manifest | 1 + osu.Desktop/app.manifest | 21 --------------------- osu.Desktop/osu.Desktop.csproj | 1 - 3 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 osu.Desktop/app.manifest diff --git a/app.manifest b/app.manifest index 533c6ff208..b85df82c4d 100644 --- a/app.manifest +++ b/app.manifest @@ -1,6 +1,7 @@  + 1 diff --git a/osu.Desktop/app.manifest b/osu.Desktop/app.manifest deleted file mode 100644 index a11cee132c..0000000000 --- a/osu.Desktop/app.manifest +++ /dev/null @@ -1,21 +0,0 @@ - - - - 1 - - - - - - - - - - - - - - true - - - diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index f1b9c92429..16d6a81d40 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -8,7 +8,6 @@ osu! osu!(lazer) lazer.ico - app.manifest 0.0.0 0.0.0 From 909edefa2048f59ffb6cf623f8fce8d16f5ca2d2 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 30 Jun 2023 21:41:18 +0200 Subject: [PATCH 0476/2100] Remove unnecessary `` osu! was working fine without this. --- app.manifest | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/app.manifest b/app.manifest index b85df82c4d..2e5ba1b18a 100644 --- a/app.manifest +++ b/app.manifest @@ -32,16 +32,4 @@ true - - - - - From bfa5bcb2a77de4b3416128b8d829e99fee06036d Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 30 Jun 2023 21:43:01 +0200 Subject: [PATCH 0477/2100] Update `` to match what osu! actually supports --- app.manifest | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/app.manifest b/app.manifest index 2e5ba1b18a..69702111ce 100644 --- a/app.manifest +++ b/app.manifest @@ -15,15 +15,9 @@ - - - - - - - + From b0e716feab3f728e4bf59b3800a86fdaf1e030ee Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 30 Jun 2023 21:59:46 +0200 Subject: [PATCH 0478/2100] Use correct `perMonitorV2` `` Yes, `xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings"` is required else the game will exit with code 500 on startup. --- app.manifest | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app.manifest b/app.manifest index 69702111ce..ad8b5d005a 100644 --- a/app.manifest +++ b/app.manifest @@ -23,7 +23,8 @@ - true + per Monitor + perMonitorV2 From 2b1d637292621f9d5edfd73b142b30bb5485a6c7 Mon Sep 17 00:00:00 2001 From: Cootz Date: Sat, 1 Jul 2023 09:48:42 +0300 Subject: [PATCH 0479/2100] Add missing ruleset store --- osu.Game.Tests/Database/BeatmapImporterTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 1f42979d11..69496630ce 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -423,6 +423,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { var importer = new BeatmapImporter(storage, realm); + using var store = new RealmRulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); From 8d25e2c3e1d419a18cf40ff99f2d9cd454991d91 Mon Sep 17 00:00:00 2001 From: Cootz Date: Sat, 1 Jul 2023 09:49:06 +0300 Subject: [PATCH 0480/2100] Add importer update test --- .../Database/BeatmapImporterUpdateTests.cs | 65 +++++++++++++++++++ osu.Game/Scoring/ScoreInfo.cs | 1 + 2 files changed, 66 insertions(+) diff --git a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs index b94cff2a9a..20fe0bfca6 100644 --- a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs @@ -347,6 +347,71 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestDandlingScoreTransferred() + { + RunTestWithRealmAsync(async (realm, storage) => + { + var importer = new BeatmapImporter(storage, realm); + using var rulesets = new RealmRulesetStore(realm, storage); + + using var __ = getBeatmapArchive(out string pathOriginal); + using var _ = getBeatmapArchive(out string pathOnlineCopy); + + var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal)); + + Assert.That(importBeforeUpdate, Is.Not.Null); + Debug.Assert(importBeforeUpdate != null); + + string scoreTargetBeatmapHash = string.Empty; + + //Set score + importBeforeUpdate.PerformWrite(s => + { + var beatmapInfo = s.Beatmaps.First(); + + scoreTargetBeatmapHash = beatmapInfo.Hash; + + s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); + }); + + //Modify beatmap + const string new_beatmap_hash = "new_hash"; + importBeforeUpdate.PerformWrite(s => + { + var beatmapInfo = s.Beatmaps.First(b => b.Hash == scoreTargetBeatmapHash); + + beatmapInfo.Hash = new_beatmap_hash; + beatmapInfo.ResetOnlineInfo(); + }); + + realm.Run(r => r.Refresh()); + + checkCount(realm, 1); + + //second import matches first before modification, + //in other words beatmap version where score have been set + var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOnlineCopy), importBeforeUpdate.Value); + + Assert.That(importAfterUpdate, Is.Not.Null); + Debug.Assert(importAfterUpdate != null); + + realm.Run(r => r.Refresh()); + + //account modified beatmap + checkCount(realm, count_beatmaps + 1); + checkCount(realm, count_beatmaps + 1); + checkCount(realm, 2); + + // score is transferred across to the new set + checkCount(realm, 1); + + //score is transferred to new beatmap + Assert.That(importBeforeUpdate.Value.Beatmaps.First(b => b.Hash == new_beatmap_hash).Scores, Has.Count.EqualTo(0)); + Assert.That(importAfterUpdate.Value.Beatmaps.First(b => b.Hash == scoreTargetBeatmapHash).Scores, Has.Count.EqualTo(1)); + }); + } + [Test] public void TestScoreLostOnModification() { diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index d56338c6a4..6dfac419e5 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -82,6 +82,7 @@ namespace osu.Game.Scoring { Ruleset = ruleset ?? new RulesetInfo(); BeatmapInfo = beatmap ?? new BeatmapInfo(); + BeatmapHash = BeatmapInfo.Hash; RealmUser = realmUser ?? new RealmUser(); ID = Guid.NewGuid(); } From a4a9223726b35d5dcba09af3fde12c58d0b3063a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 1 Jul 2023 23:12:04 +0900 Subject: [PATCH 0481/2100] Move score re-attach to `PostImport` --- osu.Game/Beatmaps/BeatmapImporter.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index bf549f40c4..b0f58d0298 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -200,21 +200,22 @@ namespace osu.Game.Beatmaps LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be disassociated and marked for deletion."); } } + } + + protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters) + { + base.PostImport(model, realm, parameters); //Because of specific score storing in Osu! database, it can already contain scores for imported beatmap. //To restore scores we need to manually reassign them to new/re-exported beatmap. - foreach (BeatmapInfo beatmap in beatmapSet.Beatmaps) + foreach (BeatmapInfo beatmap in model.Beatmaps) { IQueryable scores = realm.All().Where(score => score.BeatmapHash == beatmap.Hash); if (scores.Any()) scores.ForEach(score => score.BeatmapInfo = beatmap); //We intentionally ignore BeatmapHash because we checked hash equality } - } - protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters) - { - base.PostImport(model, realm, parameters); ProcessBeatmap?.Invoke(model, parameters.Batch ? MetadataLookupScope.LocalCacheFirst : MetadataLookupScope.OnlineFirst); } From 5bd91a531d37c4a89e7cc1925ca80888c81d276b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 1 Jul 2023 23:37:22 +0900 Subject: [PATCH 0482/2100] Tidy up comments and code --- osu.Game/Beatmaps/BeatmapImporter.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index b0f58d0298..da987eb752 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -206,14 +206,12 @@ namespace osu.Game.Beatmaps { base.PostImport(model, realm, parameters); - //Because of specific score storing in Osu! database, it can already contain scores for imported beatmap. - //To restore scores we need to manually reassign them to new/re-exported beatmap. + // Scores are stored separately from beatmaps, and persisted when a beatmap is modified or deleted. + // Let's reattach any matching scores that exist in the database, based on hash. foreach (BeatmapInfo beatmap in model.Beatmaps) { - IQueryable scores = realm.All().Where(score => score.BeatmapHash == beatmap.Hash); - - if (scores.Any()) - scores.ForEach(score => score.BeatmapInfo = beatmap); //We intentionally ignore BeatmapHash because we checked hash equality + foreach (var score in realm.All().Where(score => score.BeatmapHash == beatmap.Hash)) + score.BeatmapInfo = beatmap; } ProcessBeatmap?.Invoke(model, parameters.Batch ? MetadataLookupScope.LocalCacheFirst : MetadataLookupScope.OnlineFirst); From 5f880397a94ea968aba752e7ab658c28bab58312 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 2 Jul 2023 00:04:53 +0900 Subject: [PATCH 0483/2100] Increase the minimum size of the scroll bar Allows easier targetting when there is a lot of content in the scroll view As discussed in https://github.com/ppy/osu/discussions/24095#discussioncomment-6332398. --- osu.Game/Collections/CollectionDropdown.cs | 2 +- osu.Game/Graphics/Containers/OsuScrollContainer.cs | 11 ++++++----- osu.Game/Overlays/OverlaySidebar.cs | 2 +- .../OnlinePlay/Components/ParticipantsDisplay.cs | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game/Collections/CollectionDropdown.cs b/osu.Game/Collections/CollectionDropdown.cs index 19fa3a3d66..e95565a5c8 100644 --- a/osu.Game/Collections/CollectionDropdown.cs +++ b/osu.Game/Collections/CollectionDropdown.cs @@ -188,7 +188,7 @@ namespace osu.Game.Collections { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - X = -OsuScrollContainer.SCROLL_BAR_HEIGHT, + X = -OsuScrollContainer.SCROLL_BAR_WIDTH, Scale = new Vector2(0.65f), Action = addOrRemove, }); diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index e39fd45a16..da6996c170 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -28,7 +28,7 @@ namespace osu.Game.Graphics.Containers public partial class OsuScrollContainer : ScrollContainer where T : Drawable { - public const float SCROLL_BAR_HEIGHT = 10; + public const float SCROLL_BAR_WIDTH = 10; public const float SCROLL_BAR_PADDING = 3; /// @@ -139,6 +139,8 @@ namespace osu.Game.Graphics.Containers private readonly Box box; + protected override float MinimumDimSize => SCROLL_BAR_WIDTH * 3; + public OsuScrollbar(Direction scrollDir) : base(scrollDir) { @@ -147,7 +149,7 @@ namespace osu.Game.Graphics.Containers CornerRadius = 5; // needs to be set initially for the ResizeTo to respect minimum size - Size = new Vector2(SCROLL_BAR_HEIGHT); + Size = new Vector2(SCROLL_BAR_WIDTH); const float margin = 3; @@ -173,11 +175,10 @@ namespace osu.Game.Graphics.Containers public override void ResizeTo(float val, int duration = 0, Easing easing = Easing.None) { - Vector2 size = new Vector2(SCROLL_BAR_HEIGHT) + this.ResizeTo(new Vector2(SCROLL_BAR_WIDTH) { [(int)ScrollDirection] = val - }; - this.ResizeTo(size, duration, easing); + }, duration, easing); } protected override bool OnHover(HoverEvent e) diff --git a/osu.Game/Overlays/OverlaySidebar.cs b/osu.Game/Overlays/OverlaySidebar.cs index f1bdfbddac..070f1f0c37 100644 --- a/osu.Game/Overlays/OverlaySidebar.cs +++ b/osu.Game/Overlays/OverlaySidebar.cs @@ -28,7 +28,7 @@ namespace osu.Game.Overlays scrollbarBackground = new Box { RelativeSizeAxes = Axes.Y, - Width = OsuScrollContainer.SCROLL_BAR_HEIGHT, + Width = OsuScrollContainer.SCROLL_BAR_WIDTH, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Alpha = 0.5f diff --git a/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs index e6999771d3..5128bc4c14 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Components RelativeSizeAxes = Axes.X; scroll.RelativeSizeAxes = Axes.X; - scroll.Height = ParticipantsList.TILE_SIZE + OsuScrollContainer.SCROLL_BAR_HEIGHT + OsuScrollContainer.SCROLL_BAR_PADDING * 2; + scroll.Height = ParticipantsList.TILE_SIZE + OsuScrollContainer.SCROLL_BAR_WIDTH + OsuScrollContainer.SCROLL_BAR_PADDING * 2; list.RelativeSizeAxes = Axes.Y; list.AutoSizeAxes = Axes.X; From e38ac4185cdd630a105452d19c6bdf627e794621 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sat, 1 Jul 2023 19:02:09 +0200 Subject: [PATCH 0484/2100] Update inline with framework `IWindow` changes --- osu.Desktop/OsuGameDesktop.cs | 8 +++----- osu.Game/OsuGame.cs | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index efd3d358b7..a0db896f46 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -147,14 +147,12 @@ namespace osu.Desktop { base.SetHost(host); - var desktopWindow = (SDL2DesktopWindow)host.Window; - var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"); if (iconStream != null) - desktopWindow.SetIconFromStream(iconStream); + host.Window.SetIconFromStream(iconStream); - desktopWindow.CursorState |= CursorState.Hidden; - desktopWindow.Title = Name; + host.Window.CursorState |= CursorState.Hidden; + host.Window.Title = Name; } protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo(); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 93dd97ea15..5b654e0c16 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -289,9 +289,9 @@ namespace osu.Game { base.SetHost(host); - if (host.Window is SDL2Window sdlWindow) + if (host.Window != null) { - sdlWindow.DragDrop += path => + host.Window.DragDrop += path => { // on macOS/iOS, URL associations are handled via SDL_DROPFILE events. if (path.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) From caba571263b6fca21fde60d25ac755a8308e21ec Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sat, 1 Jul 2023 19:11:48 +0200 Subject: [PATCH 0485/2100] Remove manifest DPI awareness entires It'll be properly handled by osu!framework with https://github.com/ppy/osu/pull/24092 --- app.manifest | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app.manifest b/app.manifest index ad8b5d005a..088ad1dde7 100644 --- a/app.manifest +++ b/app.manifest @@ -21,10 +21,4 @@ - - - per Monitor - perMonitorV2 - - From 1ce60378be4ced5619533c2aaa25d46667dca777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 1 Jul 2023 20:37:33 +0200 Subject: [PATCH 0486/2100] Rewrite comments further --- osu.Game.Tests/Database/BeatmapImporterTests.cs | 5 ++++- .../Database/BeatmapImporterUpdateTests.cs | 16 +++++++++------- osu.Game/Beatmaps/BeatmapImporter.cs | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 69496630ce..84e84f030c 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -431,7 +431,7 @@ namespace osu.Game.Tests.Database await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First()); - //editor work imitation + // imitate making local changes via editor realm.Run(r => { r.Write(() => @@ -442,6 +442,9 @@ namespace osu.Game.Tests.Database }); }); + // for now, making changes to a beatmap doesn't remove the backlink from the score to the beatmap. + // the logic of ensuring that scores match the beatmap is upheld via comparing the hash in usages (see: https://github.com/ppy/osu/pull/22539). + // TODO: revisit when fixing https://github.com/ppy/osu/issues/24069. Assert.That(imported.Beatmaps.First().Scores.Any()); var importedSecondTime = await importer.Import(new ImportTask(temp)); diff --git a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs index 20fe0bfca6..8f48a96ac9 100644 --- a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs @@ -365,7 +365,7 @@ namespace osu.Game.Tests.Database string scoreTargetBeatmapHash = string.Empty; - //Set score + // set a score on the beatmap importBeforeUpdate.PerformWrite(s => { var beatmapInfo = s.Beatmaps.First(); @@ -375,7 +375,7 @@ namespace osu.Game.Tests.Database s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); }); - //Modify beatmap + // locally modify beatmap const string new_beatmap_hash = "new_hash"; importBeforeUpdate.PerformWrite(s => { @@ -387,10 +387,12 @@ namespace osu.Game.Tests.Database realm.Run(r => r.Refresh()); + // for now, making changes to a beatmap doesn't remove the backlink from the score to the beatmap. + // the logic of ensuring that scores match the beatmap is upheld via comparing the hash in usages (https://github.com/ppy/osu/pull/22539). + // TODO: revisit when fixing https://github.com/ppy/osu/issues/24069. checkCount(realm, 1); - //second import matches first before modification, - //in other words beatmap version where score have been set + // reimport the original beatmap before local modifications var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOnlineCopy), importBeforeUpdate.Value); Assert.That(importAfterUpdate, Is.Not.Null); @@ -398,15 +400,15 @@ namespace osu.Game.Tests.Database realm.Run(r => r.Refresh()); - //account modified beatmap + // both original and locally modified versions present checkCount(realm, count_beatmaps + 1); checkCount(realm, count_beatmaps + 1); checkCount(realm, 2); - // score is transferred across to the new set + // score is preserved checkCount(realm, 1); - //score is transferred to new beatmap + // score is transferred to new beatmap Assert.That(importBeforeUpdate.Value.Beatmaps.First(b => b.Hash == new_beatmap_hash).Scores, Has.Count.EqualTo(0)); Assert.That(importAfterUpdate.Value.Beatmaps.First(b => b.Hash == scoreTargetBeatmapHash).Scores, Has.Count.EqualTo(1)); }); diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index da987eb752..fd766490fc 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -206,7 +206,7 @@ namespace osu.Game.Beatmaps { base.PostImport(model, realm, parameters); - // Scores are stored separately from beatmaps, and persisted when a beatmap is modified or deleted. + // Scores are stored separately from beatmaps, and persist even when a beatmap is modified or deleted. // Let's reattach any matching scores that exist in the database, based on hash. foreach (BeatmapInfo beatmap in model.Beatmaps) { From b6e9422aa4c714799f5f5fcf26010a8a60baedba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 1 Jul 2023 20:38:00 +0200 Subject: [PATCH 0487/2100] Fix typo in test name --- osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs index 8f48a96ac9..83cb54df3f 100644 --- a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs @@ -348,7 +348,7 @@ namespace osu.Game.Tests.Database } [Test] - public void TestDandlingScoreTransferred() + public void TestDanglingScoreTransferred() { RunTestWithRealmAsync(async (realm, storage) => { From 746212be7b4c1d8b44517a1d4aeecd5709ff64f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 1 Jul 2023 20:42:34 +0200 Subject: [PATCH 0488/2100] Remove weird `.Run(r => r.Write(...))` construction --- osu.Game.Tests/Database/BeatmapImporterTests.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 84e84f030c..84e6a6c00f 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -432,14 +432,12 @@ namespace osu.Game.Tests.Database await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First()); // imitate making local changes via editor - realm.Run(r => + // ReSharper disable once MethodHasAsyncOverload + realm.Write(_ => { - r.Write(() => - { - BeatmapInfo beatmap = imported.Beatmaps.First(); - beatmap.Hash = "new_hash"; - beatmap.ResetOnlineInfo(); - }); + BeatmapInfo beatmap = imported.Beatmaps.First(); + beatmap.Hash = "new_hash"; + beatmap.ResetOnlineInfo(); }); // for now, making changes to a beatmap doesn't remove the backlink from the score to the beatmap. From 183777f8df4e0139818d7df1be50a38b5f0f848d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 1 Jul 2023 21:22:51 +0200 Subject: [PATCH 0489/2100] Fix edge cases where selection buttons go outside playfield bounds Addresses https://github.com/ppy/osu/discussions/23599#discussioncomment-6300885. --- osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index e93b9f0691..ad264dd8f0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -381,6 +381,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { buttons.Anchor = Anchor.BottomCentre; buttons.Origin = Anchor.BottomCentre; + buttons.Y = Math.Min(0, ToLocalSpace(Parent.ScreenSpaceDrawQuad.BottomLeft).Y - DrawHeight); } else if (topExcess > bottomExcess) { From 9eec1337b33169992abd67b7fc9063b7c6ce70a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 1 Jul 2023 21:30:41 +0200 Subject: [PATCH 0490/2100] Use slightly different condition for better UX --- osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index ad264dd8f0..5d9fac739c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -377,7 +377,9 @@ namespace osu.Game.Screens.Edit.Compose.Components float leftExcess = thisQuad.TopLeft.X - parentQuad.TopLeft.X; float rightExcess = parentQuad.TopRight.X - thisQuad.TopRight.X; - if (topExcess + bottomExcess < buttons.Height + button_padding) + float minHeight = buttons.ScreenSpaceDrawQuad.Height; + + if (topExcess < minHeight && bottomExcess < minHeight) { buttons.Anchor = Anchor.BottomCentre; buttons.Origin = Anchor.BottomCentre; From 95c30fe12a6906aae2475514261454dd71fa8141 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 2 Jul 2023 21:51:32 +0900 Subject: [PATCH 0491/2100] Duplicate sign out string for now --- osu.Game/Localisation/LoginPanelStrings.cs | 5 +++++ osu.Game/Overlays/Login/UserAction.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/LoginPanelStrings.cs b/osu.Game/Localisation/LoginPanelStrings.cs index 19b0ca3b52..925c2b9146 100644 --- a/osu.Game/Localisation/LoginPanelStrings.cs +++ b/osu.Game/Localisation/LoginPanelStrings.cs @@ -24,6 +24,11 @@ namespace osu.Game.Localisation /// public static LocalisableString SignedIn => new TranslatableString(getKey(@"signed_in"), @"Signed in"); + /// + /// "Sign out" + /// + public static LocalisableString SignOut => new TranslatableString(getKey(@"sign_out"), @"Sign out"); + /// /// "Account" /// diff --git a/osu.Game/Overlays/Login/UserAction.cs b/osu.Game/Overlays/Login/UserAction.cs index aa2fad6cdb..813968a053 100644 --- a/osu.Game/Overlays/Login/UserAction.cs +++ b/osu.Game/Overlays/Login/UserAction.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Login [LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.AppearOffline))] AppearOffline, - [LocalisableDescription(typeof(LayoutStrings), nameof(LayoutStrings.PopupUserLinksLogout))] + [LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.SignOut))] SignOut, } } From 5d5e6a5ab750f380393a9c6ffd9cd3028e486f1f Mon Sep 17 00:00:00 2001 From: Hydria Date: Mon, 3 Jul 2023 17:45:30 +0100 Subject: [PATCH 0492/2100] Finalised LN Adjustment Values Spent a couple days discussing this on the pp rework server about values that were the most acceptable, these seemed to be the best from the community standpoint of top players. Note: This is more to fix issues with the current system, not to be a final solution. Related Google Sheets Page: https://docs.google.com/spreadsheets/d/1P0AxfdKvMHwWBQder4ZkFGO1fC9eADSGCryA5-UGriU/edit?usp=sharing --- osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index 06c825e37d..0a4fec3a70 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills { private const double individual_decay_base = 0.125; private const double overall_decay_base = 0.30; - private const double release_threshold = 24; + private const double release_threshold = 30; protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 1; @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills // 0.0 +--------+-+---------------> Release Difference / ms // release_threshold if (isOverlapping) - holdAddition = 1 / (1 + Math.Exp(0.5 * (release_threshold - closestEndTime))); + holdAddition = 1 / (1 + Math.Exp(0.27 * (release_threshold - closestEndTime))); // Decay and increase individualStrains in own column individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base); From 67650831bd07fe8756c5595d9117ca04064b9439 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 14:19:25 +0900 Subject: [PATCH 0493/2100] Remove unnecessary null check --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 89bb908b1f..046563fad7 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -199,15 +199,13 @@ namespace osu.Game.Database if (!score.IsLegacyScore) return score.TotalScore; - var beatmap = beatmaps.GetWorkingBeatmap(score.BeatmapInfo); - var ruleset = score.Ruleset.CreateInstance(); + WorkingBeatmap beatmap = beatmaps.GetWorkingBeatmap(score.BeatmapInfo); + Ruleset ruleset = score.Ruleset.CreateInstance(); if (ruleset is not ILegacyRuleset legacyRuleset) return score.TotalScore; - var sv1Processor = legacyRuleset.CreateLegacyScoreProcessor(); - if (sv1Processor == null) - return score.TotalScore; + ILegacyScoreProcessor sv1Processor = legacyRuleset.CreateLegacyScoreProcessor(); sv1Processor.Simulate(beatmap, beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods), score.Mods); From d74b1e148dd0afae80603afb2b16e68e1c3640a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 14:50:34 +0900 Subject: [PATCH 0494/2100] Make `ScoreInfo.BeatmapInfo` nullable --- osu.Desktop/DiscordRichPresence.cs | 2 +- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- osu.Game.Tests/Models/DisplayStringTest.cs | 8 ++++---- .../Ranking/TestSceneContractedPanelMiddleContent.cs | 2 +- osu.Game/Database/RealmAccess.cs | 2 +- osu.Game/Online/Spectator/SpectatorClient.cs | 2 +- osu.Game/Scoring/IScoreInfo.cs | 2 +- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 2 +- osu.Game/Scoring/ScoreImporter.cs | 4 +++- osu.Game/Scoring/ScoreInfo.cs | 6 ++---- osu.Game/Scoring/ScoreInfoExtensions.cs | 2 +- osu.Game/Scoring/ScorePerformanceCache.cs | 2 +- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 2 +- osu.Game/Screens/Select/LocalScoreDeleteDialog.cs | 7 +------ osu.Game/Users/UserActivity.cs | 2 +- 15 files changed, 21 insertions(+), 26 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index fe3e08537e..b1e11d7a60 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -187,7 +187,7 @@ namespace osu.Desktop return edit.BeatmapInfo.ToString() ?? string.Empty; case UserActivity.WatchingReplay watching: - return watching.BeatmapInfo.ToString(); + return watching.BeatmapInfo?.ToString() ?? string.Empty; case UserActivity.InLobby lobby: return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index a193bacde5..ac4462c18b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss; // TODO: The detection of rulesets is temporary until the leftover old skills have been reworked. - bool isConvert = score.BeatmapInfo.Ruleset.OnlineID != 1; + bool isConvert = score.BeatmapInfo!.Ruleset.OnlineID != 1; double multiplier = 1.13; diff --git a/osu.Game.Tests/Models/DisplayStringTest.cs b/osu.Game.Tests/Models/DisplayStringTest.cs index d585a0eb9f..b5303e1dd6 100644 --- a/osu.Game.Tests/Models/DisplayStringTest.cs +++ b/osu.Game.Tests/Models/DisplayStringTest.cs @@ -87,10 +87,10 @@ namespace osu.Game.Tests.Models var mock = new Mock(); mock.Setup(m => m.User).Returns(new APIUser { Username = "user" }); // TODO: temporary. - mock.Setup(m => m.Beatmap.Metadata.Artist).Returns("artist"); - mock.Setup(m => m.Beatmap.Metadata.Title).Returns("title"); - mock.Setup(m => m.Beatmap.Metadata.Author.Username).Returns("author"); - mock.Setup(m => m.Beatmap.DifficultyName).Returns("difficulty"); + mock.Setup(m => m.Beatmap!.Metadata.Artist).Returns("artist"); + mock.Setup(m => m.Beatmap!.Metadata.Title).Returns("title"); + mock.Setup(m => m.Beatmap!.Metadata.Author.Username).Returns("author"); + mock.Setup(m => m.Beatmap!.DifficultyName).Returns("difficulty"); Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("user playing artist - title (author) [difficulty]")); } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs index 3004cb8a0c..0f17b08b7b 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("show excess mods score", () => { var score = TestResources.CreateTestScoreInfo(); - score.Mods = score.BeatmapInfo.Ruleset.CreateInstance().CreateAllMods().ToArray(); + score.Mods = score.BeatmapInfo!.Ruleset.CreateInstance().CreateAllMods().ToArray(); showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), score); }); } diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index da4caa42ba..f8b3f24a72 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -896,7 +896,7 @@ namespace osu.Game.Database var scores = migration.NewRealm.All(); foreach (var score in scores) - score.BeatmapHash = score.BeatmapInfo.Hash; + score.BeatmapHash = score.BeatmapInfo?.Hash ?? string.Empty; break; } diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 89da8b9d32..14e137caf1 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -185,7 +185,7 @@ namespace osu.Game.Online.Spectator IsPlaying = true; // transfer state at point of beginning play - currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID; + currentState.BeatmapID = score.ScoreInfo.BeatmapInfo!.OnlineID; currentState.RulesetID = score.ScoreInfo.RulesetID; currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); currentState.State = SpectatedUserState.Playing; diff --git a/osu.Game/Scoring/IScoreInfo.cs b/osu.Game/Scoring/IScoreInfo.cs index 3644d099d9..d17558f800 100644 --- a/osu.Game/Scoring/IScoreInfo.cs +++ b/osu.Game/Scoring/IScoreInfo.cs @@ -28,7 +28,7 @@ namespace osu.Game.Scoring double? PP { get; } - IBeatmapInfo Beatmap { get; } + IBeatmapInfo? Beatmap { get; } IRulesetInfo Ruleset { get; } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index f71da6c7e0..6cad8716b1 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -65,7 +65,7 @@ namespace osu.Game.Scoring.Legacy { sw.Write((byte)(score.ScoreInfo.Ruleset.OnlineID)); sw.Write(LATEST_VERSION); - sw.Write(score.ScoreInfo.BeatmapInfo.MD5Hash); + sw.Write(score.ScoreInfo.BeatmapInfo!.MD5Hash); sw.Write(score.ScoreInfo.User.Username); sw.Write(FormattableString.Invariant($"lazer-{score.ScoreInfo.User.Username}-{score.ScoreInfo.Date}").ComputeMD5Hash()); sw.Write((ushort)(score.ScoreInfo.GetCount300() ?? 0)); diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 16658a598a..5770da10be 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -64,6 +64,8 @@ namespace osu.Game.Scoring protected override void Populate(ScoreInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) { + Debug.Assert(model.BeatmapInfo != null); + // Ensure the beatmap is not detached. if (!model.BeatmapInfo.IsManaged) model.BeatmapInfo = realm.Find(model.BeatmapInfo.ID); @@ -99,7 +101,7 @@ namespace osu.Game.Scoring if (score.MaximumStatistics.Select(kvp => kvp.Value).Sum() > 0) return; - var beatmap = score.BeatmapInfo.Detach(); + var beatmap = score.BeatmapInfo?.Detach(); var ruleset = score.Ruleset.Detach(); var rulesetInstance = ruleset.CreateInstance(); diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 6dfac419e5..6816e86e9d 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -36,7 +36,7 @@ namespace osu.Game.Scoring /// /// When setting this, make sure to also set to allow relational consistency when a beatmap is potentially changed. /// - public BeatmapInfo BeatmapInfo { get; set; } = null!; + public BeatmapInfo? BeatmapInfo { get; set; } /// /// The at the point in time when the score was set. @@ -129,14 +129,12 @@ namespace osu.Game.Scoring public int RankInt { get; set; } IRulesetInfo IScoreInfo.Ruleset => Ruleset; - IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo; + IBeatmapInfo? IScoreInfo.Beatmap => BeatmapInfo; IUser IScoreInfo.User => User; IEnumerable IHasNamedFiles.Files => Files; #region Properties required to make things work with existing usages - public Guid BeatmapInfoID => BeatmapInfo.ID; - public int UserID => RealmUser.OnlineID; public int RulesetID => Ruleset.OnlineID; diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs index 85598076d6..6e57a9fd0b 100644 --- a/osu.Game/Scoring/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/ScoreInfoExtensions.cs @@ -13,7 +13,7 @@ namespace osu.Game.Scoring /// /// A user-presentable display title representing this score. /// - public static string GetDisplayTitle(this IScoreInfo scoreInfo) => $"{scoreInfo.User.Username} playing {scoreInfo.Beatmap.GetDisplayTitle()}"; + public static string GetDisplayTitle(this IScoreInfo scoreInfo) => $"{scoreInfo.User.Username} playing {scoreInfo.Beatmap?.GetDisplayTitle() ?? "unknown"}"; /// /// Orders an array of s by total score. diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs index bdbcfe4efe..1f2b1aeb95 100644 --- a/osu.Game/Scoring/ScorePerformanceCache.cs +++ b/osu.Game/Scoring/ScorePerformanceCache.cs @@ -34,7 +34,7 @@ namespace osu.Game.Scoring { var score = lookup.ScoreInfo; - var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, token).ConfigureAwait(false); + var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, token).ConfigureAwait(false); // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. if (attributes?.Attributes == null) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index c8920a734d..f187b8a302 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -63,7 +63,7 @@ namespace osu.Game.Screens.Ranking protected override APIRequest? FetchScores(Action>? scoresCallback) { - if (Score.BeatmapInfo.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending) + if (Score.BeatmapInfo!.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending) return null; getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); diff --git a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs index c4add31a4f..cd98872b65 100644 --- a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs +++ b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs @@ -4,9 +4,7 @@ using osu.Framework.Allocation; using osu.Game.Overlays.Dialog; using osu.Game.Scoring; -using System.Diagnostics; using osu.Framework.Graphics.Sprites; -using osu.Game.Beatmaps; namespace osu.Game.Screens.Select { @@ -20,11 +18,8 @@ namespace osu.Game.Screens.Select } [BackgroundDependencyLoader] - private void load(BeatmapManager beatmapManager, ScoreManager scoreManager) + private void load(ScoreManager scoreManager) { - BeatmapInfo? beatmapInfo = beatmapManager.QueryBeatmap(b => b.ID == score.BeatmapInfoID); - Debug.Assert(beatmapInfo != null); - BodyText = $"{score.User} ({score.DisplayAccuracy}, {score.Rank})"; Icon = FontAwesome.Regular.TrashAlt; diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 1761282e2e..c82f642fdc 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -111,7 +111,7 @@ namespace osu.Game.Users protected string Username => score.User.Username; - public BeatmapInfo BeatmapInfo => score.BeatmapInfo; + public BeatmapInfo? BeatmapInfo => score.BeatmapInfo; public WatchingReplay(ScoreInfo score) { From f30dc59afec1c670e4c791026224a4c50862e6e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 14:50:50 +0900 Subject: [PATCH 0495/2100] Update tests to show expected score retention behaviour when saving a beatmap --- osu.Game.Tests/Database/BeatmapImporterTests.cs | 10 +++++----- osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs | 5 ++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 84e6a6c00f..84d13ac85e 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -431,19 +431,18 @@ namespace osu.Game.Tests.Database await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First()); + Assert.That(imported.Beatmaps.First().Scores.Any()); + // imitate making local changes via editor // ReSharper disable once MethodHasAsyncOverload - realm.Write(_ => + realm.Write(r => { BeatmapInfo beatmap = imported.Beatmaps.First(); beatmap.Hash = "new_hash"; beatmap.ResetOnlineInfo(); }); - // for now, making changes to a beatmap doesn't remove the backlink from the score to the beatmap. - // the logic of ensuring that scores match the beatmap is upheld via comparing the hash in usages (see: https://github.com/ppy/osu/pull/22539). - // TODO: revisit when fixing https://github.com/ppy/osu/issues/24069. - Assert.That(imported.Beatmaps.First().Scores.Any()); + Assert.That(!imported.Beatmaps.First().Scores.Any()); var importedSecondTime = await importer.Import(new ImportTask(temp)); @@ -461,6 +460,7 @@ namespace osu.Game.Tests.Database Assert.That(importedFirstTimeBeatmap.Hash != importedSecondTimeBeatmap.Hash); Assert.That(!importedFirstTimeBeatmap.Scores.Any()); Assert.That(importedSecondTimeBeatmap.Scores.Count() == 1); + Assert.That(importedSecondTimeBeatmap.Scores.Single().BeatmapInfo, Is.EqualTo(importedSecondTimeBeatmap)); }); } diff --git a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs index 83cb54df3f..3934a67be0 100644 --- a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs @@ -387,10 +387,9 @@ namespace osu.Game.Tests.Database realm.Run(r => r.Refresh()); - // for now, making changes to a beatmap doesn't remove the backlink from the score to the beatmap. - // the logic of ensuring that scores match the beatmap is upheld via comparing the hash in usages (https://github.com/ppy/osu/pull/22539). - // TODO: revisit when fixing https://github.com/ppy/osu/issues/24069. + // making changes to a beatmap doesn't remove the score from realm, but should disassociate the beatmap. checkCount(realm, 1); + Assert.That(realm.Run(r => r.All().First().BeatmapInfo), Is.Null); // reimport the original beatmap before local modifications var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOnlineCopy), importBeforeUpdate.Value); From 64fc5e40e8377b4a38e3ef28f9e8544021485566 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 14:51:09 +0900 Subject: [PATCH 0496/2100] Move score attach logic to a helper method and call during editor save --- osu.Game.Tests/Database/BeatmapImporterTests.cs | 1 + .../Database/BeatmapImporterUpdateTests.cs | 1 + osu.Game/Beatmaps/BeatmapImporter.cs | 4 +--- osu.Game/Beatmaps/BeatmapInfo.cs | 16 ++++++++++++++++ osu.Game/Beatmaps/BeatmapManager.cs | 3 +++ 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 84d13ac85e..1440f540b4 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -440,6 +440,7 @@ namespace osu.Game.Tests.Database BeatmapInfo beatmap = imported.Beatmaps.First(); beatmap.Hash = "new_hash"; beatmap.ResetOnlineInfo(); + beatmap.UpdateLocalScores(r); }); Assert.That(!imported.Beatmaps.First().Scores.Any()); diff --git a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs index 3934a67be0..e1bf8f5eae 100644 --- a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs @@ -383,6 +383,7 @@ namespace osu.Game.Tests.Database beatmapInfo.Hash = new_beatmap_hash; beatmapInfo.ResetOnlineInfo(); + beatmapInfo.UpdateLocalScores(s.Realm); }); realm.Run(r => r.Refresh()); diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index fd766490fc..d20027892f 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -20,7 +20,6 @@ using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; -using osu.Game.Scoring; using Realms; namespace osu.Game.Beatmaps @@ -210,8 +209,7 @@ namespace osu.Game.Beatmaps // Let's reattach any matching scores that exist in the database, based on hash. foreach (BeatmapInfo beatmap in model.Beatmaps) { - foreach (var score in realm.All().Where(score => score.BeatmapHash == beatmap.Hash)) - score.BeatmapInfo = beatmap; + beatmap.UpdateLocalScores(realm); } ProcessBeatmap?.Invoke(model, parameters.Batch ? MetadataLookupScope.LocalCacheFirst : MetadataLookupScope.OnlineFirst); diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 5019d64276..c1aeec1f71 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -234,6 +234,22 @@ namespace osu.Game.Beatmaps } } + /// + /// Local scores are retained separate from a beatmap's lifetime, matched via . + /// Therefore we need to detach / reattach scores when a beatmap is edited or imported. + /// + /// A realm instance in an active write transaction. + public void UpdateLocalScores(Realm realm) + { + // first disassociate any scores which are already attached and no longer valid. + foreach (var score in Scores) + score.BeatmapInfo = null; + + // then attach any scores which match the new hash. + foreach (var score in realm.All().Where(s => s.BeatmapHash == Hash)) + score.BeatmapInfo = this; + } + IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata; IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet; IRulesetInfo IBeatmapInfo.Ruleset => Ruleset; diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 73811b2e62..295f6ed91a 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -467,6 +467,9 @@ namespace osu.Game.Beatmaps if (transferCollections) beatmapInfo.TransferCollectionReferences(r, oldMd5Hash); + liveBeatmapSet.Beatmaps.Single(b => b.ID == beatmapInfo.ID) + .UpdateLocalScores(r); + // do not look up metadata. // this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst. ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None); From a0bed0fceccb370700b98c559572c7f4f5dbbd08 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 15:18:51 +0900 Subject: [PATCH 0497/2100] Add full flow test of `UpdateLocalScores` --- .../Database/BeatmapImporterTests.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 1440f540b4..2766e4509e 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -417,6 +417,60 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestImport_Modify_Revert() + { + RunTestWithRealmAsync(async (realm, storage) => + { + var importer = new BeatmapImporter(storage, realm); + using var store = new RealmRulesetStore(realm, storage); + + var imported = await LoadOszIntoStore(importer, realm.Realm); + + await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First()); + + var score = realm.Run(r => r.All().Single()); + + string originalHash = imported.Beatmaps.First().Hash; + const string modified_hash = "new_hash"; + + Assert.That(imported.Beatmaps.First().Scores.Single(), Is.EqualTo(score)); + + Assert.That(score.BeatmapHash, Is.EqualTo(originalHash)); + Assert.That(score.BeatmapInfo, Is.EqualTo(imported.Beatmaps.First())); + + // imitate making local changes via editor + // ReSharper disable once MethodHasAsyncOverload + realm.Write(r => + { + BeatmapInfo beatmap = imported.Beatmaps.First(); + beatmap.Hash = modified_hash; + beatmap.ResetOnlineInfo(); + beatmap.UpdateLocalScores(r); + }); + + Assert.That(!imported.Beatmaps.First().Scores.Any()); + + Assert.That(score.BeatmapInfo, Is.Null); + Assert.That(score.BeatmapHash, Is.EqualTo(originalHash)); + + // imitate making local changes via editor + // ReSharper disable once MethodHasAsyncOverload + realm.Write(r => + { + BeatmapInfo beatmap = imported.Beatmaps.First(); + beatmap.Hash = originalHash; + beatmap.ResetOnlineInfo(); + beatmap.UpdateLocalScores(r); + }); + + Assert.That(imported.Beatmaps.First().Scores.Single(), Is.EqualTo(score)); + + Assert.That(score.BeatmapHash, Is.EqualTo(originalHash)); + Assert.That(score.BeatmapInfo, Is.EqualTo(imported.Beatmaps.First())); + }); + } + [Test] public void TestImport_ThenModifyMapWithScore_ThenImport() { From 1a6381bcbb26fc649a99a88cb07368ae0b249e7b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 15:35:09 +0900 Subject: [PATCH 0498/2100] Reduce code repetition for sleep logic --- osu.Game/BackgroundBeatmapProcessor.cs | 33 +++++++++++++------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 11e6a4619b..9a2d029724 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -132,11 +132,7 @@ namespace osu.Game foreach (var id in beatmapSetIds) { - while (localUserPlayInfo?.IsPlaying.Value == true) - { - Logger.Log("Background processing sleeping due to active gameplay..."); - Thread.Sleep(TimeToSleepDuringGameplay); - } + sleepIfRequired(); realmAccess.Run(r => { @@ -177,11 +173,7 @@ namespace osu.Game foreach (var id in scoreIds) { - while (localUserPlayInfo?.IsPlaying.Value == true) - { - Logger.Log("Background processing sleeping due to active gameplay..."); - Thread.Sleep(TimeToSleepDuringGameplay); - } + sleepIfRequired(); try { @@ -229,19 +221,17 @@ namespace osu.Game ProgressNotification? notification = null; - if (scoreIds.Count > 0) - notificationOverlay?.Post(notification = new ProgressNotification { State = ProgressNotificationState.Active }); + if (scoreIds.Count == 0) + return; + + notificationOverlay?.Post(notification = new ProgressNotification { State = ProgressNotificationState.Active }); int count = 0; updateNotification(); foreach (var id in scoreIds) { - while (localUserPlayInfo?.IsPlaying.Value == true) - { - Logger.Log("Background processing sleeping due to active gameplay..."); - Thread.Sleep(TimeToSleepDuringGameplay); - } + sleepIfRequired(); try { @@ -286,5 +276,14 @@ namespace osu.Game } } } + + private void sleepIfRequired() + { + while (localUserPlayInfo?.IsPlaying.Value == true) + { + Logger.Log("Background processing sleeping due to active gameplay..."); + Thread.Sleep(TimeToSleepDuringGameplay); + } + } } } From 3b5f3b67a7672a07896ce50a83a43d1b089a2ff0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 15:42:04 +0900 Subject: [PATCH 0499/2100] Tidy up and improve messaging on completion notification --- osu.Game/BackgroundBeatmapProcessor.cs | 40 +++++++++++--------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 9a2d029724..b3fb938f48 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -219,18 +219,20 @@ namespace osu.Game Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); - ProgressNotification? notification = null; - if (scoreIds.Count == 0) return; - notificationOverlay?.Post(notification = new ProgressNotification { State = ProgressNotificationState.Active }); + ProgressNotification notification = new ProgressNotification { State = ProgressNotificationState.Active }; - int count = 0; - updateNotification(); + notificationOverlay?.Post(notification); + + int processedCount = 0; foreach (var id in scoreIds) { + notification.Text = $"Upgrading scores to new scoring algorithm ({processedCount} of {scoreIds.Count})"; + notification.Progress = (float)processedCount / scoreIds.Count; + sleepIfRequired(); try @@ -248,32 +250,24 @@ namespace osu.Game }); Logger.Log($"Converted total score for score {id}"); + ++processedCount; } catch (Exception e) { Logger.Log($"Failed to convert total score for {id}: {e}"); } - - ++count; - updateNotification(); } - void updateNotification() + if (processedCount == scoreIds.Count) { - if (notification == null) - return; - - if (count == scoreIds.Count) - { - notification.CompletionText = $"Total score updated for {scoreIds.Count} scores"; - notification.Progress = 1; - notification.State = ProgressNotificationState.Completed; - } - else - { - notification.Text = $"Total score updated for {count} of {scoreIds.Count} scores"; - notification.Progress = (float)count / scoreIds.Count; - } + notification.CompletionText = $"{processedCount} score(s) have been upgraded to the new scoring algorithm"; + notification.Progress = 1; + notification.State = ProgressNotificationState.Completed; + } + else + { + notification.CompletionText = $"{processedCount} of {scoreIds.Count} score(s) have been upgraded to the new scoring algorithm. Check logs for issues with remaining scores."; + notification.State = ProgressNotificationState.Cancelled; } } From 16290241114691382a91116ba15e926043496afb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 17:32:54 +0900 Subject: [PATCH 0500/2100] `ILegacyScoreProcessor` -> `ILegacyScoreSimulator` --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 +- .../Difficulty/CatchDifficultyCalculator.cs | 10 +++++----- ...yScoreProcessor.cs => CatchLegacyScoreSimulator.cs} | 2 +- .../Difficulty/ManiaDifficultyCalculator.cs | 10 +++++----- ...yScoreProcessor.cs => ManiaLegacyScoreSimulator.cs} | 2 +- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- .../Difficulty/OsuDifficultyCalculator.cs | 10 +++++----- ...acyScoreProcessor.cs => OsuLegacyScoreSimulator.cs} | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- .../Difficulty/TaikoDifficultyCalculator.cs | 10 +++++----- ...yScoreProcessor.cs => TaikoLegacyScoreSimulator.cs} | 2 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- osu.Game/Database/StandardisedScoreMigrationTools.cs | 10 +++++----- osu.Game/Rulesets/ILegacyRuleset.cs | 2 +- ...egacyScoreProcessor.cs => ILegacyScoreSimulator.cs} | 5 ++++- 15 files changed, 38 insertions(+), 35 deletions(-) rename osu.Game.Rulesets.Catch/Difficulty/{CatchLegacyScoreProcessor.cs => CatchLegacyScoreSimulator.cs} (98%) rename osu.Game.Rulesets.Mania/Difficulty/{ManiaLegacyScoreProcessor.cs => ManiaLegacyScoreSimulator.cs} (93%) rename osu.Game.Rulesets.Osu/Difficulty/{OsuLegacyScoreProcessor.cs => OsuLegacyScoreSimulator.cs} (99%) rename osu.Game.Rulesets.Taiko/Difficulty/{TaikoLegacyScoreProcessor.cs => TaikoLegacyScoreSimulator.cs} (99%) rename osu.Game/Rulesets/Scoring/{ILegacyScoreProcessor.cs => ILegacyScoreSimulator.cs} (90%) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 9862b7d886..8f1a1b8ef5 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -202,7 +202,7 @@ namespace osu.Game.Rulesets.Catch public int LegacyID => 2; - public ILegacyScoreProcessor CreateLegacyScoreProcessor() => new CatchLegacyScoreProcessor(); + public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new CatchLegacyScoreSimulator(); public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame(); diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 446a76486b..0b56405299 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -51,11 +51,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty if (ComputeLegacyScoringValues) { - CatchLegacyScoreProcessor sv1Processor = new CatchLegacyScoreProcessor(); - sv1Processor.Simulate(workingBeatmap, beatmap, mods); - attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; - attributes.LegacyComboScore = sv1Processor.ComboScore; - attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; + CatchLegacyScoreSimulator sv1Simulator = new CatchLegacyScoreSimulator(); + sv1Simulator.Simulate(workingBeatmap, beatmap, mods); + attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore; + attributes.LegacyComboScore = sv1Simulator.ComboScore; + attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio; } return attributes; diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreProcessor.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs similarity index 98% rename from osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreProcessor.cs rename to osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs index 67a813300d..c79fd36d96 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Difficulty { - internal class CatchLegacyScoreProcessor : ILegacyScoreProcessor + internal class CatchLegacyScoreSimulator : ILegacyScoreSimulator { public int AccuracyScore { get; private set; } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index d7994e6a0c..de9f0d91ae 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -62,11 +62,11 @@ namespace osu.Game.Rulesets.Mania.Difficulty if (ComputeLegacyScoringValues) { - ManiaLegacyScoreProcessor sv1Processor = new ManiaLegacyScoreProcessor(); - sv1Processor.Simulate(workingBeatmap, beatmap, mods); - attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; - attributes.LegacyComboScore = sv1Processor.ComboScore; - attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; + ManiaLegacyScoreSimulator sv1Simulator = new ManiaLegacyScoreSimulator(); + sv1Simulator.Simulate(workingBeatmap, beatmap, mods); + attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore; + attributes.LegacyComboScore = sv1Simulator.ComboScore; + attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio; } return attributes; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreProcessor.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs similarity index 93% rename from osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreProcessor.cs rename to osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs index e30d06c7b0..e544428979 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Difficulty { - internal class ManiaLegacyScoreProcessor : ILegacyScoreProcessor + internal class ManiaLegacyScoreSimulator : ILegacyScoreSimulator { public int AccuracyScore => 0; public int ComboScore { get; private set; } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 77cc3e06d2..2e96c89516 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -302,7 +302,7 @@ namespace osu.Game.Rulesets.Mania public int LegacyID => 3; - public ILegacyScoreProcessor CreateLegacyScoreProcessor() => new ManiaLegacyScoreProcessor(); + public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new ManiaLegacyScoreSimulator(); public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame(); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index e28dbd96ac..b92092c674 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -111,11 +111,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (ComputeLegacyScoringValues) { - OsuLegacyScoreProcessor sv1Processor = new OsuLegacyScoreProcessor(); - sv1Processor.Simulate(workingBeatmap, beatmap, mods); - attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; - attributes.LegacyComboScore = sv1Processor.ComboScore; - attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; + OsuLegacyScoreSimulator sv1Simulator = new OsuLegacyScoreSimulator(); + sv1Simulator.Simulate(workingBeatmap, beatmap, mods); + attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore; + attributes.LegacyComboScore = sv1Simulator.ComboScore; + attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio; } return attributes; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreProcessor.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs similarity index 99% rename from osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreProcessor.cs rename to osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs index a5e12e5564..980d86e4ad 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Difficulty { - internal class OsuLegacyScoreProcessor : ILegacyScoreProcessor + internal class OsuLegacyScoreSimulator : ILegacyScoreSimulator { public int AccuracyScore { get; private set; } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index abbd4a43c8..b44d999d4f 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -253,7 +253,7 @@ namespace osu.Game.Rulesets.Osu public int LegacyID => 0; - public ILegacyScoreProcessor CreateLegacyScoreProcessor() => new OsuLegacyScoreProcessor(); + public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new OsuLegacyScoreSimulator(); public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame(); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 28268d9a13..25adba5ab6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -101,11 +101,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (ComputeLegacyScoringValues) { - TaikoLegacyScoreProcessor sv1Processor = new TaikoLegacyScoreProcessor(); - sv1Processor.Simulate(workingBeatmap, beatmap, mods); - attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; - attributes.LegacyComboScore = sv1Processor.ComboScore; - attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; + TaikoLegacyScoreSimulator sv1Simulator = new TaikoLegacyScoreSimulator(); + sv1Simulator.Simulate(workingBeatmap, beatmap, mods); + attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore; + attributes.LegacyComboScore = sv1Simulator.ComboScore; + attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio; } return attributes; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs similarity index 99% rename from osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreProcessor.cs rename to osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs index c9f508f5e9..e77327d622 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty { - internal class TaikoLegacyScoreProcessor : ILegacyScoreProcessor + internal class TaikoLegacyScoreSimulator : ILegacyScoreSimulator { public int AccuracyScore { get; private set; } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index af02c94d38..aa31b1924f 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Taiko public int LegacyID => 1; - public ILegacyScoreProcessor CreateLegacyScoreProcessor() => new TaikoLegacyScoreProcessor(); + public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new TaikoLegacyScoreSimulator(); public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 046563fad7..7ab90c337c 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -205,15 +205,15 @@ namespace osu.Game.Database if (ruleset is not ILegacyRuleset legacyRuleset) return score.TotalScore; - ILegacyScoreProcessor sv1Processor = legacyRuleset.CreateLegacyScoreProcessor(); + ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator(); - sv1Processor.Simulate(beatmap, beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods), score.Mods); + sv1Simulator.Simulate(beatmap, beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods), score.Mods); return ConvertFromLegacyTotalScore(score, new DifficultyAttributes { - LegacyAccuracyScore = sv1Processor.AccuracyScore, - LegacyComboScore = sv1Processor.ComboScore, - LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio + LegacyAccuracyScore = sv1Simulator.AccuracyScore, + LegacyComboScore = sv1Simulator.ComboScore, + LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio }); } diff --git a/osu.Game/Rulesets/ILegacyRuleset.cs b/osu.Game/Rulesets/ILegacyRuleset.cs index ba12c1f559..24aa672219 100644 --- a/osu.Game/Rulesets/ILegacyRuleset.cs +++ b/osu.Game/Rulesets/ILegacyRuleset.cs @@ -14,6 +14,6 @@ namespace osu.Game.Rulesets /// int LegacyID { get; } - ILegacyScoreProcessor CreateLegacyScoreProcessor(); + ILegacyScoreSimulator CreateLegacyScoreSimulator(); } } diff --git a/osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ILegacyScoreSimulator.cs similarity index 90% rename from osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs rename to osu.Game/Rulesets/Scoring/ILegacyScoreSimulator.cs index c689d3610d..7240f0d73e 100644 --- a/osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ILegacyScoreSimulator.cs @@ -7,7 +7,10 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Scoring { - public interface ILegacyScoreProcessor + /// + /// Generates attributes which are required to calculate old-style Score V1 scores. + /// + public interface ILegacyScoreSimulator { /// /// The accuracy portion of the legacy (ScoreV1) total score. From a0c3fa9c138a15f668b06a5ca5eea3b1a7722090 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 17:53:53 +0900 Subject: [PATCH 0501/2100] Move preconditions to realm migration step to simplify marker version logic --- osu.Game/BackgroundBeatmapProcessor.cs | 17 +---------------- osu.Game/Database/RealmAccess.cs | 13 ++++++------- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 2 +- 3 files changed, 8 insertions(+), 24 deletions(-) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index b3fb938f48..3af6f0771c 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -14,7 +14,6 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Extensions; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; @@ -199,23 +198,9 @@ namespace osu.Game private void convertLegacyTotalScoreToStandardised() { - HashSet scoreIds = new HashSet(); - Logger.Log("Querying for scores that need total score conversion..."); - realmAccess.Run(r => - { - foreach (var score in r.All().Where(s => s.IsLegacyScore)) - { - if (!score.Ruleset.IsLegacyRuleset()) - continue; - - if (score.Version >= 30000003) - continue; - - scoreIds.Add(score.ID); - } - }); + HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All().Where(s => s.Version == 30000002).Select(s => s.ID))); Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 93d70d7aea..95297e9cd6 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -970,16 +970,15 @@ namespace osu.Game.Database case 31: { - var scores = migration.NewRealm.All(); - - foreach (var score in scores) + foreach (var score in migration.NewRealm.All()) { - if (score.IsLegacyScore) + if (score.IsLegacyScore && score.Ruleset.IsLegacyRuleset()) { - score.LegacyTotalScore = score.TotalScore; - - // Scores with this version will trigger the update process in BackgroundBeatmapProcessor. + // Scores with this version will trigger the score upgrade process in BackgroundBeatmapProcessor. score.Version = 30000002; + + // Set a sane default while background processing runs. + score.LegacyTotalScore = score.TotalScore; } else score.Version = LegacyScoreEncoder.LATEST_VERSION; diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index a5ac151cf8..ef033bf5bd 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -28,7 +28,7 @@ namespace osu.Game.Scoring.Legacy /// /// /// 30000001: Appends to the end of scores. - /// 30000002: Score stored to replay calculated using the Score V2 algorithm. + /// 30000002: Score stored to replay calculated using the Score V2 algorithm. Legacy scores on this version are candidate to Score V1 -> V2 conversion. /// 30000003: First version after converting legacy total score to standardised. /// /// From 4de15f975e1acf5bd91077f8464f35aba68c3805 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 18:08:26 +0900 Subject: [PATCH 0502/2100] Fix realm silly business --- osu.Game/BackgroundBeatmapProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 3af6f0771c..0b8323eb41 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -200,7 +200,7 @@ namespace osu.Game { Logger.Log("Querying for scores that need total score conversion..."); - HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All().Where(s => s.Version == 30000002).Select(s => s.ID))); + HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All().Where(s => s.Version == 30000002).AsEnumerable().Select(s => s.ID))); Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); From 257a96ef604a494122ec564a20405bbe6ad63be8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 18:21:22 +0900 Subject: [PATCH 0503/2100] Fix background beatmap processor thread not correctly exiting --- osu.Game/BackgroundBeatmapProcessor.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 0b8323eb41..f5e3f721f7 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -189,6 +189,10 @@ namespace osu.Game Logger.Log($"Populated maximum statistics for score {id}"); } + catch (ObjectDisposedException) + { + throw; + } catch (Exception e) { Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}"); @@ -237,6 +241,10 @@ namespace osu.Game Logger.Log($"Converted total score for score {id}"); ++processedCount; } + catch (ObjectDisposedException) + { + throw; + } catch (Exception e) { Logger.Log($"Failed to convert total score for {id}: {e}"); From 56bfb92ba656ccb216bc10c64a1fb5e0144bc847 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 18:22:10 +0900 Subject: [PATCH 0504/2100] Allow user cancellation --- osu.Game/BackgroundBeatmapProcessor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index f5e3f721f7..ca33a74c57 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -219,6 +219,9 @@ namespace osu.Game foreach (var id in scoreIds) { + if (notification.State == ProgressNotificationState.Cancelled) + break; + notification.Text = $"Upgrading scores to new scoring algorithm ({processedCount} of {scoreIds.Count})"; notification.Progress = (float)processedCount / scoreIds.Count; From d3eb06578e96a75be4521fe92a461d043aaa3d45 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 18:34:53 +0900 Subject: [PATCH 0505/2100] Improve messaging around failed scores --- osu.Game/BackgroundBeatmapProcessor.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index ca33a74c57..018b1352b2 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -216,6 +216,7 @@ namespace osu.Game notificationOverlay?.Post(notification); int processedCount = 0; + int failedCount = 0; foreach (var id in scoreIds) { @@ -251,6 +252,7 @@ namespace osu.Game catch (Exception e) { Logger.Log($"Failed to convert total score for {id}: {e}"); + ++failedCount; } } @@ -262,7 +264,12 @@ namespace osu.Game } else { - notification.CompletionText = $"{processedCount} of {scoreIds.Count} score(s) have been upgraded to the new scoring algorithm. Check logs for issues with remaining scores."; + notification.Text = $"{processedCount} of {scoreIds.Count} score(s) have been upgraded to the new scoring algorithm."; + + // We may have arrived here due to user cancellation or completion with failures. + if (failedCount > 0) + notification.Text += $" Check logs for issues with {failedCount} failed upgrades."; + notification.State = ProgressNotificationState.Cancelled; } } From dd9998127eb8e1d9899bca50cbd6b6fbf8af0650 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 18:35:03 +0900 Subject: [PATCH 0506/2100] Count missing beatmaps as errored items --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 7ab90c337c..60530c31cb 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -205,9 +205,14 @@ namespace osu.Game.Database if (ruleset is not ILegacyRuleset legacyRuleset) return score.TotalScore; + var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods); + + if (playableBeatmap.HitObjects.Count == 0) + throw new InvalidOperationException("Beatmap contains no hit objects!"); + ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator(); - sv1Simulator.Simulate(beatmap, beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods), score.Mods); + sv1Simulator.Simulate(beatmap, playableBeatmap, score.Mods); return ConvertFromLegacyTotalScore(score, new DifficultyAttributes { From 664294cef4728d4b35d51fa58cf9480913a2a10d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 18:39:19 +0900 Subject: [PATCH 0507/2100] Fix cancelled progress notifications requiring exit confirmation --- osu.Game/Overlays/INotificationOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/INotificationOverlay.cs b/osu.Game/Overlays/INotificationOverlay.cs index 6a1b66bbd2..c5ff10c619 100644 --- a/osu.Game/Overlays/INotificationOverlay.cs +++ b/osu.Game/Overlays/INotificationOverlay.cs @@ -44,6 +44,6 @@ namespace osu.Game.Overlays /// /// All ongoing operations (ie. any not in a completed state). /// - public IEnumerable OngoingOperations => AllNotifications.OfType().Where(p => p.State != ProgressNotificationState.Completed); + public IEnumerable OngoingOperations => AllNotifications.OfType().Where(p => p.State != ProgressNotificationState.Completed && p.State != ProgressNotificationState.Cancelled); } } From aee89e5e4bacfbfe4263f941457f28837a595135 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 19:59:57 +0900 Subject: [PATCH 0508/2100] Rewrite comment regarding `LegacyTotalScore` --- osu.Game/Database/RealmAccess.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 95297e9cd6..02abed2495 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -977,7 +977,7 @@ namespace osu.Game.Database // Scores with this version will trigger the score upgrade process in BackgroundBeatmapProcessor. score.Version = 30000002; - // Set a sane default while background processing runs. + // Transfer known legacy scores to a permanent storage field for preservation. score.LegacyTotalScore = score.TotalScore; } else From f2aa80f4138452c4a859e463f780c1c87b106cc1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 20:02:25 +0900 Subject: [PATCH 0509/2100] Rename and adjust xmldoc on `TotalScoreVersion` --- osu.Game/BackgroundBeatmapProcessor.cs | 4 ++-- osu.Game/Database/RealmAccess.cs | 4 ++-- osu.Game/Scoring/ScoreInfo.cs | 24 +++++++++++++----------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 018b1352b2..9fe3a41b03 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -204,7 +204,7 @@ namespace osu.Game { Logger.Log("Querying for scores that need total score conversion..."); - HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All().Where(s => s.Version == 30000002).AsEnumerable().Select(s => s.ID))); + HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All().Where(s => s.TotalScoreVersion == 30000002).AsEnumerable().Select(s => s.ID))); Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); @@ -239,7 +239,7 @@ namespace osu.Game { ScoreInfo s = r.Find(id); s.TotalScore = newTotalScore; - s.Version = LegacyScoreEncoder.LATEST_VERSION; + s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION; }); Logger.Log($"Converted total score for score {id}"); diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 02abed2495..2bc932f307 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -975,13 +975,13 @@ namespace osu.Game.Database if (score.IsLegacyScore && score.Ruleset.IsLegacyRuleset()) { // Scores with this version will trigger the score upgrade process in BackgroundBeatmapProcessor. - score.Version = 30000002; + score.TotalScoreVersion = 30000002; // Transfer known legacy scores to a permanent storage field for preservation. score.LegacyTotalScore = score.TotalScore; } else - score.Version = LegacyScoreEncoder.LATEST_VERSION; + score.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION; } break; diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 94376300fa..eddd1bb80a 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -54,13 +54,25 @@ namespace osu.Game.Scoring public long TotalScore { get; set; } + /// + /// The version of processing applied to calculate total score as stored in the database. + /// If this does not match , + /// the total score has not yet been updated to reflect the current scoring values. + /// + /// See 's conversion logic. + /// + /// + /// This may not match the version stored in the replay files. + /// + internal int TotalScoreVersion { get; set; } = LegacyScoreEncoder.LATEST_VERSION; + /// /// Used to preserve the total score for legacy scores. /// /// /// Not populated if is false. /// - public long? LegacyTotalScore { get; set; } + internal long? LegacyTotalScore { get; set; } public int MaxCombo { get; set; } @@ -72,16 +84,6 @@ namespace osu.Game.Scoring public double? PP { get; set; } - /// - /// The version of this score as stored in the database. - /// If this does not match , - /// then the score has not yet been updated to reflect the current scoring values. - /// - /// - /// This may not match the version stored in the replay files. - /// - public int Version { get; set; } = LegacyScoreEncoder.LATEST_VERSION; - [Indexed] public long OnlineID { get; set; } = -1; From a55809733dd1c9ec05f2a9ecfadcc97da827c17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jul 2023 22:20:50 +0200 Subject: [PATCH 0510/2100] Expand `ScoreInfo.BeatmapInfo` xmldoc --- osu.Game/Scoring/ScoreInfo.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 6816e86e9d..4798a105c5 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -34,7 +34,14 @@ namespace osu.Game.Scoring /// The this score was made against. /// /// - /// When setting this, make sure to also set to allow relational consistency when a beatmap is potentially changed. + /// + /// This property may be if the score was set on a beatmap (or a version of the beatmap) that is not available locally + /// e.g. due to online updates, or local modifications to the beatmap. + /// The property will only link to a if its matches . + /// + /// + /// Due to the above, whenever setting this, make sure to also set to allow relational consistency when a beatmap is potentially changed. + /// /// public BeatmapInfo? BeatmapInfo { get; set; } From bcdbdf57efe1218e2f378a2a4830425bf508a0b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jul 2023 22:22:57 +0200 Subject: [PATCH 0511/2100] Reword comment --- osu.Game.Tests/Database/BeatmapImporterTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 2766e4509e..0eac70f9c8 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -454,7 +454,7 @@ namespace osu.Game.Tests.Database Assert.That(score.BeatmapInfo, Is.Null); Assert.That(score.BeatmapHash, Is.EqualTo(originalHash)); - // imitate making local changes via editor + // imitate reverting the local changes made above // ReSharper disable once MethodHasAsyncOverload realm.Write(r => { From e2ddcb23497468c45d1285797984fb531d0cef1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jul 2023 22:39:26 +0200 Subject: [PATCH 0512/2100] Silence a few remaining nullability warnings --- .../Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs | 2 +- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 2 +- osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs | 2 +- .../Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs | 2 +- osu.Game/Screens/Play/SoloPlayer.cs | 2 +- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index c05774400f..d71c72f4ec 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Ranking var author = new RealmUser { Username = "mapper_name" }; var score = TestResources.CreateTestScoreInfo(createTestBeatmap(author)); - score.Mods = score.BeatmapInfo.Ruleset.CreateInstance().CreateAllMods().ToArray(); + score.Mods = score.BeatmapInfo!.Ruleset.CreateInstance().CreateAllMods().ToArray(); showPanel(score); }); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 42068ff117..c5b61c1a90 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -405,7 +405,7 @@ namespace osu.Game.Tests.Visual.Ranking public UnrankedSoloResultsScreen(ScoreInfo score) : base(score, true) { - Score.BeatmapInfo.OnlineID = 0; + Score.BeatmapInfo!.OnlineID = 0; Score.BeatmapInfo.Status = BeatmapOnlineStatus.Pending; } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 425f40258e..615a3e39af 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -163,7 +163,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }, username, #pragma warning disable 618 - new StatisticText(score.MaxCombo, score.BeatmapInfo.MaxCombo, @"0\x"), + new StatisticText(score.MaxCombo, score.BeatmapInfo!.MaxCombo, @"0\x"), #pragma warning restore 618 }; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index e030b1e34f..c92b79cb4d 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -123,7 +123,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores accuracyColumn.Text = value.DisplayAccuracy; maxComboColumn.Text = value.MaxCombo.ToLocalisableString(@"0\x"); - ppColumn.Alpha = value.BeatmapInfo.Status.GrantsPerformancePoints() ? 1 : 0; + ppColumn.Alpha = value.BeatmapInfo!.Status.GrantsPerformancePoints() ? 1 : 0; if (value.PP is double pp) ppColumn.Text = pp.ToLocalisableString(@"N0"); diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index dafdf00136..f7ae3eb62b 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Play { IBeatmapInfo beatmap = score.ScoreInfo.BeatmapInfo; - Debug.Assert(beatmap.OnlineID > 0); + Debug.Assert(beatmap!.OnlineID > 0); return new SubmitSoloScoreRequest(score.ScoreInfo, token, beatmap.OnlineID); } diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 82c429798e..d1dc1a81db 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Ranking.Expanded [BackgroundDependencyLoader] private void load(BeatmapDifficultyCache beatmapDifficultyCache) { - var beatmap = score.BeatmapInfo; + var beatmap = score.BeatmapInfo!; var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata; string creator = metadata.Author.Username; From 6dc8c7b617038a87898c0a847788c4cd502c7c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Jul 2023 22:26:41 +0200 Subject: [PATCH 0513/2100] Add `HitObjectLifetimeEntry.NestedEntries` --- osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs index b517f6b9e6..4e058e7c31 100644 --- a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs +++ b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics.Performance; using osu.Game.Rulesets.Judgements; @@ -19,6 +20,11 @@ namespace osu.Game.Rulesets.Objects /// public readonly HitObject HitObject; + /// + /// The list of for the 's nested objects (if any). + /// + public readonly List NestedEntries = new List(); + /// /// The result that was judged with. /// This is set by the accompanying , and reused when required for rewinding. From 2b098bdf6120120e8f1985bf98d16ea26a578f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jul 2023 23:23:23 +0200 Subject: [PATCH 0514/2100] Add test coverage for mixed pooled/non-pooled usages --- .../Gameplay/TestScenePoolingRuleset.cs | 85 ++++++++++++++++--- 1 file changed, 73 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index d16f51f36e..e3afb91040 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -170,7 +170,16 @@ namespace osu.Game.Tests.Visual.Gameplay ManualClock clock = null; var beatmap = new Beatmap(); - beatmap.HitObjects.Add(new TestHitObjectWithNested { Duration = 40 }); + beatmap.HitObjects.Add(new TestHitObjectWithNested + { + Duration = 40, + NestedObjects = new HitObject[] + { + new PooledNestedHitObject { StartTime = 10 }, + new PooledNestedHitObject { StartTime = 20 }, + new PooledNestedHitObject { StartTime = 30 } + } + }); createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock())); @@ -209,6 +218,44 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("object judged", () => playfield.JudgedObjects.Count == 1); } + [Test] + public void TestPooledObjectWithNonPooledNesteds() + { + ManualClock clock = null; + TestHitObjectWithNested hitObjectWithNested; + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(hitObjectWithNested = new TestHitObjectWithNested + { + Duration = 40, + NestedObjects = new HitObject[] + { + new PooledNestedHitObject { StartTime = 10 }, + new NonPooledNestedHitObject { StartTime = 20 }, + new NonPooledNestedHitObject { StartTime = 30 } + } + }); + + createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock())); + + AddAssert("hitobject entry has all nesteds", () => playfield.HitObjectContainer.Entries.Single().NestedEntries, () => Has.Count.EqualTo(3)); + + AddStep("skip to middle of object", () => clock.CurrentTime = (hitObjectWithNested.StartTime + hitObjectWithNested.GetEndTime()) / 2); + AddAssert("2 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(2)); + + AddStep("skip to before end of object", () => clock.CurrentTime = hitObjectWithNested.GetEndTime() - 1); + AddAssert("3 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3)); + + AddStep("removing object doesn't crash", () => playfield.Remove(hitObjectWithNested)); + AddStep("clear judged", () => playfield.JudgedObjects.Clear()); + AddStep("add object back", () => playfield.Add(hitObjectWithNested)); + + AddStep("skip to long past object", () => clock.CurrentTime = 100_000); + // the parent entry should still be linked to nested entries of pooled objects that are managed externally + // but not contain synthetic entries that were created for the non-pooled objects. + AddAssert("entry still has non-synthetic nested entries", () => playfield.HitObjectContainer.Entries.Single().NestedEntries, () => Has.Count.EqualTo(1)); + } + private void createTest(IBeatmap beatmap, int poolSize, Func createClock = null) { AddStep("create test", () => @@ -289,7 +336,7 @@ namespace osu.Game.Tests.Visual.Gameplay RegisterPool(poolSize); RegisterPool(poolSize); RegisterPool(poolSize); - RegisterPool(poolSize); + RegisterPool(poolSize); } protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); @@ -422,16 +469,22 @@ namespace osu.Game.Tests.Visual.Gameplay private class TestHitObjectWithNested : TestHitObject { + public IEnumerable NestedObjects { get; init; } = Array.Empty(); + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { base.CreateNestedHitObjects(cancellationToken); - for (int i = 0; i < 3; ++i) - AddNested(new NestedHitObject { StartTime = (float)Duration * (i + 1) / 4 }); + foreach (var ho in NestedObjects) + AddNested(ho); } } - private class NestedHitObject : ConvertHitObject + private class PooledNestedHitObject : ConvertHitObject + { + } + + private class NonPooledNestedHitObject : ConvertHitObject { } @@ -482,6 +535,9 @@ namespace osu.Game.Tests.Visual.Gameplay nestedContainer.Clear(false); } + protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) + => hitObject is NonPooledNestedHitObject nonPooled ? new DrawableNestedHitObject(nonPooled) : null; + protected override void CheckForResult(bool userTriggered, double timeOffset) { base.CheckForResult(userTriggered, timeOffset); @@ -490,25 +546,30 @@ namespace osu.Game.Tests.Visual.Gameplay } } - private partial class DrawableNestedHitObject : DrawableHitObject + private partial class DrawableNestedHitObject : DrawableHitObject { public DrawableNestedHitObject() - : this(null) { } - public DrawableNestedHitObject(NestedHitObject hitObject) + public DrawableNestedHitObject(PooledNestedHitObject hitObject) + : base(hitObject) + { + } + + public DrawableNestedHitObject(NonPooledNestedHitObject hitObject) : base(hitObject) { - Size = new Vector2(15); - Colour = Colour4.White; - RelativePositionAxes = Axes.Both; - Origin = Anchor.Centre; } [BackgroundDependencyLoader] private void load() { + Size = new Vector2(15); + Colour = Colour4.White; + RelativePositionAxes = Axes.Both; + Origin = Anchor.Centre; + AddInternal(new Circle { RelativeSizeAxes = Axes.Both, From bae7670855a9060d9eec38628039ceca573a6feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Jul 2023 22:33:11 +0200 Subject: [PATCH 0515/2100] Redirect `HitObjectEntryManager` child mapping to HOLE --- .../Objects/HitObjectLifetimeEntry.cs | 2 +- .../Objects/Pooling/HitObjectEntryManager.cs | 35 +++++++------------ 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs index 4e058e7c31..69a78c6bd0 100644 --- a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs +++ b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Objects /// /// The list of for the 's nested objects (if any). /// - public readonly List NestedEntries = new List(); + public List NestedEntries { get; internal set; } = new List(); /// /// The result that was judged with. diff --git a/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs b/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs index 6c39ea44da..08f693bae3 100644 --- a/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs +++ b/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs @@ -43,11 +43,6 @@ namespace osu.Game.Rulesets.Objects.Pooling /// private readonly Dictionary parentMap = new Dictionary(); - /// - /// Stores the list of child entries for each hit object managed by this . - /// - private readonly Dictionary> childrenMap = new Dictionary>(); - public void Add(HitObjectLifetimeEntry entry, HitObject? parent) { HitObject hitObject = entry.HitObject; @@ -57,14 +52,13 @@ namespace osu.Game.Rulesets.Objects.Pooling // Add the entry. entryMap[hitObject] = entry; - childrenMap[hitObject] = new List(); // If the entry has a parent, set it and add the entry to the parent's children. if (parent != null) { parentMap[entry] = parent; - if (childrenMap.TryGetValue(parent, out var parentChildEntries)) - parentChildEntries.Add(entry); + if (entryMap.TryGetValue(parent, out var parentEntry)) + parentEntry.NestedEntries.Add(entry); } hitObject.DefaultsApplied += onDefaultsApplied; @@ -81,15 +75,12 @@ namespace osu.Game.Rulesets.Objects.Pooling entryMap.Remove(hitObject); // If the entry has a parent, unset it and remove the entry from the parents' children. - if (parentMap.Remove(entry, out var parent) && childrenMap.TryGetValue(parent, out var parentChildEntries)) - parentChildEntries.Remove(entry); + if (parentMap.Remove(entry, out var parent) && entryMap.TryGetValue(parent, out var parentEntry)) + parentEntry.NestedEntries.Remove(entry); // Remove all the entries' children. - if (childrenMap.Remove(hitObject, out var childEntries)) - { - foreach (var childEntry in childEntries) - Remove(childEntry); - } + foreach (var childEntry in entry.NestedEntries) + Remove(childEntry); hitObject.DefaultsApplied -= onDefaultsApplied; OnEntryRemoved?.Invoke(entry, parent); @@ -105,16 +96,16 @@ namespace osu.Game.Rulesets.Objects.Pooling /// private void onDefaultsApplied(HitObject hitObject) { - if (!childrenMap.Remove(hitObject, out var childEntries)) + if (!entryMap.TryGetValue(hitObject, out var entry)) return; - // Remove all the entries' children. At this point the parents' (this entries') children list has been removed from the map, so this does not cause upwards traversal. - foreach (var entry in childEntries) - Remove(entry); + // Replace the entire list rather than clearing to prevent circular traversal later. + var previousEntries = entry.NestedEntries; + entry.NestedEntries = new List(); - // The removed children list needs to be added back to the map for the entry to potentially receive children. - childEntries.Clear(); - childrenMap[hitObject] = childEntries; + // Remove all the entries' children. At this point the parents' (this entries') children list has been reconstructed, so this does not cause upwards traversal. + foreach (var nested in previousEntries) + Remove(nested); } } } From 0ceaf3c451e0135d529d7ddba9b8e55069d62031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Jul 2023 22:39:39 +0200 Subject: [PATCH 0516/2100] Ensure synthetic entries from non-pooled DHO are linked to parents --- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 12 ++++++++++++ .../Objects/Pooling/HitObjectEntryManager.cs | 6 +++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 07c0d1f8a1..41d46fe85d 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -218,6 +218,8 @@ namespace osu.Game.Rulesets.Objects.Drawables protected sealed override void OnApply(HitObjectLifetimeEntry entry) { + Debug.Assert(Entry != null); + // LifetimeStart is already computed using HitObjectLifetimeEntry's InitialLifetimeOffset. // We override this with DHO's InitialLifetimeOffset for a non-pooled DHO. if (entry is SyntheticHitObjectEntry) @@ -247,6 +249,12 @@ namespace osu.Game.Rulesets.Objects.Drawables drawableNested.ParentHitObject = this; nestedHitObjects.Add(drawableNested); + + // assume that synthetic entries are not pooled and therefore need to be managed from within the DHO. + // this is important for the correctness of value of flags such as `AllJudged`. + if (drawableNested.Entry is SyntheticHitObjectEntry syntheticNestedEntry) + Entry.NestedEntries.Add(syntheticNestedEntry); + AddNestedHitObject(drawableNested); } @@ -290,6 +298,8 @@ namespace osu.Game.Rulesets.Objects.Drawables protected sealed override void OnFree(HitObjectLifetimeEntry entry) { + Debug.Assert(Entry != null); + StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable); if (HitObject is IHasComboInformation combo) @@ -318,6 +328,8 @@ namespace osu.Game.Rulesets.Objects.Drawables } nestedHitObjects.Clear(); + // clean up synthetic entries manually added in `Apply()`. + Entry.NestedEntries.RemoveAll(nestedEntry => nestedEntry is SyntheticHitObjectEntry); ClearNestedHitObjects(); HitObject.DefaultsApplied -= onDefaultsApplied; diff --git a/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs b/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs index 08f693bae3..fabf4fc444 100644 --- a/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs +++ b/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs @@ -65,8 +65,11 @@ namespace osu.Game.Rulesets.Objects.Pooling OnEntryAdded?.Invoke(entry, parent); } - public void Remove(HitObjectLifetimeEntry entry) + public bool Remove(HitObjectLifetimeEntry entry) { + if (entry is SyntheticHitObjectEntry) + return false; + HitObject hitObject = entry.HitObject; if (!entryMap.ContainsKey(hitObject)) @@ -84,6 +87,7 @@ namespace osu.Game.Rulesets.Objects.Pooling hitObject.DefaultsApplied -= onDefaultsApplied; OnEntryRemoved?.Invoke(entry, parent); + return true; } public bool TryGet(HitObject hitObject, [MaybeNullWhen(false)] out HitObjectLifetimeEntry entry) From 6c4e52821de1214a0c8aa75e47f5d7c1fe64f6b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Jul 2023 22:42:50 +0200 Subject: [PATCH 0517/2100] Redirect judgement-related flags from DHO to HOLE --- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 10 +++++----- osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs | 12 ++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 41d46fe85d..08dcf91e52 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Objects.Drawables public virtual bool DisplayResult => true; /// - /// Whether this and all of its nested s have been judged. + /// The scoring result of this . /// - public bool AllJudged => Judged && NestedHitObjects.All(h => h.AllJudged); + public JudgementResult Result => Entry?.Result; /// /// Whether this has been hit. This occurs if is hit. @@ -112,12 +112,12 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Whether this has been judged. /// Note: This does NOT include nested hitobjects. /// - public bool Judged => Result?.HasResult ?? true; + public bool Judged => Entry?.Judged ?? true; /// - /// The scoring result of this . + /// Whether this and all of its nested s have been judged. /// - public JudgementResult Result => Entry?.Result; + public bool AllJudged => Entry?.AllJudged ?? true; /// /// The relative X position of this hit object for sample playback balance adjustment. diff --git a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs index 69a78c6bd0..1d99e7440f 100644 --- a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs +++ b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics.Performance; using osu.Game.Rulesets.Judgements; @@ -31,6 +32,17 @@ namespace osu.Game.Rulesets.Objects /// internal JudgementResult? Result; + /// + /// Whether has been judged. + /// Note: This does NOT include nested hitobjects. + /// + public bool Judged => Result?.HasResult ?? true; + + /// + /// Whether and all of its nested objects have been judged. + /// + public bool AllJudged => Judged && NestedEntries.All(h => h.AllJudged); + private readonly IBindable startTimeBindable = new BindableDouble(); internal event Action? RevertResult; From b0f6b22fa7872b4eab09b6782af2297568ec17ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jul 2023 23:48:46 +0200 Subject: [PATCH 0518/2100] Add assertions covering correctness of judged flags on entry --- osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index e3afb91040..fea7456472 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -242,18 +242,23 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("skip to middle of object", () => clock.CurrentTime = (hitObjectWithNested.StartTime + hitObjectWithNested.GetEndTime()) / 2); AddAssert("2 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(2)); + AddAssert("entry not all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.False); AddStep("skip to before end of object", () => clock.CurrentTime = hitObjectWithNested.GetEndTime() - 1); AddAssert("3 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3)); + AddAssert("entry not all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.False); AddStep("removing object doesn't crash", () => playfield.Remove(hitObjectWithNested)); AddStep("clear judged", () => playfield.JudgedObjects.Clear()); + AddStep("add object back", () => playfield.Add(hitObjectWithNested)); + AddAssert("entry not all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.False); AddStep("skip to long past object", () => clock.CurrentTime = 100_000); // the parent entry should still be linked to nested entries of pooled objects that are managed externally // but not contain synthetic entries that were created for the non-pooled objects. AddAssert("entry still has non-synthetic nested entries", () => playfield.HitObjectContainer.Entries.Single().NestedEntries, () => Has.Count.EqualTo(1)); + AddAssert("entry all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.True); } private void createTest(IBeatmap beatmap, int poolSize, Func createClock = null) From 5947c2b298096e936eefb70943615153c20f5a87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 16:07:12 +0900 Subject: [PATCH 0519/2100] Throw if a null `BeatmapInfo` arrives during score import process --- osu.Game/Scoring/ScoreImporter.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 5770da10be..5a52f72cd5 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -98,10 +98,12 @@ namespace osu.Game.Scoring /// The score to populate the statistics of. public void PopulateMaximumStatistics(ScoreInfo score) { + Debug.Assert(score.BeatmapInfo != null); + if (score.MaximumStatistics.Select(kvp => kvp.Value).Sum() > 0) return; - var beatmap = score.BeatmapInfo?.Detach(); + var beatmap = score.BeatmapInfo!.Detach(); var ruleset = score.Ruleset.Detach(); var rulesetInstance = ruleset.CreateInstance(); From 3b9d7af9ee284ec5269da4495eb9ed2e4dfb85fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 17:25:31 +0900 Subject: [PATCH 0520/2100] Fix taiko hit overlay animation timing not accounting for timing section start time --- .../Skinning/Legacy/LegacyCirclePiece.cs | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs index 37eb95b86f..5516e025cd 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -8,6 +9,7 @@ using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; @@ -26,11 +28,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private Bindable currentCombo { get; } = new BindableInt(); private int animationFrame; - private double beatLength; // required for editor blueprints (not sure why these circle pieces are zero size). public override Quad ScreenSpaceDrawQuad => backgroundLayer.ScreenSpaceDrawQuad; + private TimingControlPoint timingPoint = TimingControlPoint.DEFAULT; + public LegacyCirclePiece() { RelativeSizeAxes = Axes.Both; @@ -39,11 +42,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy [Resolved(canBeNull: true)] private GameplayState? gameplayState { get; set; } - [Resolved(canBeNull: true)] - private IBeatSyncProvider? beatSyncProvider { get; set; } - [BackgroundDependencyLoader] - private void load(ISkinSource skin, DrawableHitObject drawableHitObject) + private void load(ISkinSource skin, DrawableHitObject drawableHitObject, IBeatSyncProvider? beatSyncProvider) { Drawable? getDrawableFor(string lookup) { @@ -64,6 +64,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy if (foregroundLayer != null) AddInternal(foregroundLayer); + drawableHitObject.StartTimeBindable.BindValueChanged(startTime => + { + timingPoint = beatSyncProvider?.ControlPoints?.TimingPointAt(startTime.NewValue) ?? TimingControlPoint.DEFAULT; + }, true); + // Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat). // For now just stop at first frame for sanity. foreach (var c in InternalChildren) @@ -115,14 +120,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy return; } - if (beatSyncProvider?.ControlPoints != null) - { - beatLength = beatSyncProvider.ControlPoints.TimingPointAt(Time.Current).BeatLength; - - animationFrame = Time.Current % ((beatLength * 2) / multiplier) >= beatLength / multiplier ? 0 : 1; - - animatableForegroundLayer.GotoFrame(animationFrame); - } + animationFrame = Math.Abs(Time.Current - timingPoint.Time) % ((timingPoint.BeatLength * 2) / multiplier) >= timingPoint.BeatLength / multiplier ? 0 : 1; + animatableForegroundLayer.GotoFrame(animationFrame); } private Color4 accentColour; From 3f8dfc7cb035adc38e849ac1fc895739aec86b6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 18:02:03 +0900 Subject: [PATCH 0521/2100] Fix fallback for `Judged` to be more correct Without this change, when the `Judged` value is checked on an `HitObjectLifetimeEntry` it would return `true` if a `DrawableHitObject` has not yet been associated with the entry. Which is completely wrong. Of note, the usage in `DrawableHitObject` will have never fallen through to this incorrect value as they always have a result populated: https://github.com/ppy/osu/blob/f26f001e1d01ca6bb53225da7bf06c0ad21153c5/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs#L721-L726 --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 4 ++-- osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 08dcf91e52..e4d8eb2335 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -112,12 +112,12 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Whether this has been judged. /// Note: This does NOT include nested hitobjects. /// - public bool Judged => Entry?.Judged ?? true; + public bool Judged => Entry?.Judged ?? false; /// /// Whether this and all of its nested s have been judged. /// - public bool AllJudged => Entry?.AllJudged ?? true; + public bool AllJudged => Entry?.AllJudged ?? false; /// /// The relative X position of this hit object for sample playback balance adjustment. diff --git a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs index 1d99e7440f..4450f026b4 100644 --- a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs +++ b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Objects /// Whether has been judged. /// Note: This does NOT include nested hitobjects. /// - public bool Judged => Result?.HasResult ?? true; + public bool Judged => Result?.HasResult ?? false; /// /// Whether and all of its nested objects have been judged. From e21dc56fcbc307b517ee51491d06a29cc8f07e9c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 18:20:25 +0900 Subject: [PATCH 0522/2100] Add test coverage of `Judged` state --- .../Gameplay/TestSceneDrawableHitObject.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs index 04fc4cafbd..10dbede2e0 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; @@ -80,7 +81,9 @@ namespace osu.Game.Tests.Gameplay { TestLifetimeEntry entry = null; AddStep("Create entry", () => entry = new TestLifetimeEntry(new HitObject()) { LifetimeStart = 1 }); + assertJudged(() => entry, false); AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty())); + assertJudged(() => entry, false); AddAssert("Lifetime is updated", () => entry.LifetimeStart == -TestLifetimeEntry.INITIAL_LIFETIME_OFFSET); TestDrawableHitObject dho = null; @@ -91,6 +94,7 @@ namespace osu.Game.Tests.Gameplay }); AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty())); AddAssert("Lifetime is correct", () => dho.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY && entry.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY); + assertJudged(() => entry, false); } [Test] @@ -138,6 +142,29 @@ namespace osu.Game.Tests.Gameplay AddAssert("DHO state is correct", () => dho.State.Value == ArmedState.Miss); } + [Test] + public void TestJudgedStateThroughLifetime() + { + TestDrawableHitObject dho = null; + HitObjectLifetimeEntry lifetimeEntry = null; + + AddStep("Create lifetime entry", () => lifetimeEntry = new HitObjectLifetimeEntry(new HitObject { StartTime = Time.Current })); + + assertJudged(() => lifetimeEntry, false); + + AddStep("Create DHO and apply entry", () => + { + Child = dho = new TestDrawableHitObject(); + dho.Apply(lifetimeEntry); + }); + + assertJudged(() => lifetimeEntry, false); + + AddStep("Apply result", () => dho.MissForcefully()); + + assertJudged(() => lifetimeEntry, true); + } + [Test] public void TestResultSetBeforeLoadComplete() { @@ -154,15 +181,20 @@ namespace osu.Game.Tests.Gameplay } }; }); + assertJudged(() => lifetimeEntry, true); AddStep("Create DHO and apply entry", () => { dho = new TestDrawableHitObject(); dho.Apply(lifetimeEntry); Child = dho; }); + assertJudged(() => lifetimeEntry, true); AddAssert("DHO state is correct", () => dho.State.Value, () => Is.EqualTo(ArmedState.Hit)); } + private void assertJudged(Func entry, bool val) => + AddAssert(val ? "Is judged" : "Not judged", () => entry().Judged, () => Is.EqualTo(val)); + private partial class TestDrawableHitObject : DrawableHitObject { public const double INITIAL_LIFETIME_OFFSET = 100; From 9a7bf1beddecdcdb5281f2a1cf62be26f1f513bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 18:44:01 +0900 Subject: [PATCH 0523/2100] Fix reversed order of sample return --- osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index a04c4b60f2..6098db4f7a 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -28,8 +28,8 @@ namespace osu.Game.Rulesets.Taiko.UI { PlaySamples(new ISampleInfo[] { - hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH), - baseSample + baseSample, + hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH) }); } else From f54eb8d7fa7c3b97514afcee2fa7bb38f3a31b62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 15:07:07 +0900 Subject: [PATCH 0524/2100] Move `DrumSamplePlayer` to be a skinnable component --- .../Skinning/Argon/TaikoArgonSkinTransformer.cs | 3 +++ .../Skinning/Legacy/TaikoLegacySkinTransformer.cs | 3 +++ osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs | 3 ++- osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs | 14 +++++++++----- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 5 ++++- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index 780018af4e..7e3b0e99b6 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -60,6 +60,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon // the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield. return Drawable.Empty().With(d => d.Expire()); + case TaikoSkinComponents.DrumSamplePlayer: + return Drawable.Empty(); + case TaikoSkinComponents.TaikoExplosionGreat: case TaikoSkinComponents.TaikoExplosionMiss: case TaikoSkinComponents.TaikoExplosionOk: diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index d61f9ac35d..894b91e9ce 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -52,6 +52,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy return null; + case TaikoSkinComponents.DrumSamplePlayer: + return null; + case TaikoSkinComponents.CentreHit: case TaikoSkinComponents.RimHit: if (hasHitCircle) diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index b8e3313e1b..28133ffcb2 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Taiko TaikoExplosionKiai, Scroller, Mascot, - KiaiGlow + KiaiGlow, + DrumSamplePlayer } } diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 6454fb5afa..f1dcd23698 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; @@ -12,13 +13,16 @@ namespace osu.Game.Rulesets.Taiko.UI { internal partial class DrumSamplePlayer : CompositeDrawable, IKeyBindingHandler { - private readonly DrumSampleTriggerSource leftRimSampleTriggerSource; - private readonly DrumSampleTriggerSource leftCentreSampleTriggerSource; - private readonly DrumSampleTriggerSource rightCentreSampleTriggerSource; - private readonly DrumSampleTriggerSource rightRimSampleTriggerSource; + private DrumSampleTriggerSource leftRimSampleTriggerSource = null!; + private DrumSampleTriggerSource leftCentreSampleTriggerSource = null!; + private DrumSampleTriggerSource rightCentreSampleTriggerSource = null!; + private DrumSampleTriggerSource rightRimSampleTriggerSource = null!; - public DrumSamplePlayer(HitObjectContainer hitObjectContainer) + [BackgroundDependencyLoader] + private void load(DrawableRuleset drawableRuleset) { + var hitObjectContainer = drawableRuleset.Playfield.HitObjectContainer; + InternalChildren = new Drawable[] { leftRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer), diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 9f9debe7d7..23ffac1f63 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -170,7 +170,10 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.Both, }, drumRollHitContainer.CreateProxy(), - new DrumSamplePlayer(HitObjectContainer), + new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumSamplePlayer), _ => new DrumSamplePlayer()) + { + RelativeSizeAxes = Axes.Both, + }, // this is added at the end of the hierarchy to receive input before taiko objects. // but is proxied below everything to not cover visual effects such as hit explosions. inputDrum, From 6d4fa6569f7e189368d345605c5ddf9ab0b4f0d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 16:13:32 +0900 Subject: [PATCH 0525/2100] Add back required pieces to `GameplaySampleTriggerSource` from old PR --- .../UI/GameplaySampleTriggerSource.cs | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index c554318357..efbf823058 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -34,14 +34,19 @@ namespace osu.Game.Rulesets.UI [Resolved] private IGameplayClock? gameplayClock { get; set; } + protected readonly AudioContainer AudioContainer; + public GameplaySampleTriggerSource(HitObjectContainer hitObjectContainer) { this.hitObjectContainer = hitObjectContainer; - InternalChild = hitSounds = new Container + InternalChild = AudioContainer = new AudioContainer { - Name = "concurrent sample pool", - ChildrenEnumerable = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new PausableSkinnableSound()) + Child = hitSounds = new Container + { + Name = "concurrent sample pool", + ChildrenEnumerable = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new PausableSkinnableSound()) + } }; } @@ -64,11 +69,22 @@ namespace osu.Game.Rulesets.UI protected virtual void PlaySamples(ISampleInfo[] samples) => Schedule(() => { - var hitSound = getNextSample(); - hitSound.Samples = samples; + var hitSound = GetNextSample(); + ApplySampleInfo(hitSound, samples); hitSound.Play(); }); + protected virtual void ApplySampleInfo(SkinnableSound hitSound, ISampleInfo[] samples) + { + hitSound.Samples = samples; + } + + public void StopAllPlayback() => Schedule(() => + { + foreach (var sound in hitSounds) + sound.Stop(); + }); + protected HitObject? GetMostValidObject() { if (mostValidObject == null || isAlreadyHit(mostValidObject)) From ae86fc736a51d6e5c8d454ade8fe5a028664dc8f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 16:03:11 +0900 Subject: [PATCH 0526/2100] Add argon-specific `DrumSamplePlayer` --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 49 +++++++++++++++++++ .../Argon/TaikoArgonSkinTransformer.cs | 2 +- .../UI/DrumSamplePlayer.cs | 13 +++-- .../UI/DrumSampleTriggerSource.cs | 2 +- 4 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs new file mode 100644 index 0000000000..5b690be780 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Audio; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public partial class ArgonDrumSamplePlayer : DrumSamplePlayer + { + protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer) => + new ArgonDrumSampleTriggerSource(hitObjectContainer); + + public partial class ArgonDrumSampleTriggerSource : DrumSampleTriggerSource + { + public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer) + : base(hitObjectContainer) + { + } + + public override void Play(HitType hitType) + { + // let the magic begin... + + TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; + + if (hitObject == null) + return; + + var baseSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); + + if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject) + { + PlaySamples(new ISampleInfo[] + { + hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH), + baseSample + }); + } + else + { + PlaySamples(new ISampleInfo[] { baseSample }); + } + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index 7e3b0e99b6..9fcecd2b1a 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon return Drawable.Empty().With(d => d.Expire()); case TaikoSkinComponents.DrumSamplePlayer: - return Drawable.Empty(); + return new ArgonDrumSamplePlayer(); case TaikoSkinComponents.TaikoExplosionGreat: case TaikoSkinComponents.TaikoExplosionMiss: diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index f1dcd23698..b60ad209ff 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Taiko.UI { - internal partial class DrumSamplePlayer : CompositeDrawable, IKeyBindingHandler + public partial class DrumSamplePlayer : CompositeDrawable, IKeyBindingHandler { private DrumSampleTriggerSource leftRimSampleTriggerSource = null!; private DrumSampleTriggerSource leftCentreSampleTriggerSource = null!; @@ -25,13 +25,16 @@ namespace osu.Game.Rulesets.Taiko.UI InternalChildren = new Drawable[] { - leftRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer), - leftCentreSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer), - rightCentreSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer), - rightRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer), + leftRimSampleTriggerSource = CreateTriggerSource(hitObjectContainer), + leftCentreSampleTriggerSource = CreateTriggerSource(hitObjectContainer), + rightCentreSampleTriggerSource = CreateTriggerSource(hitObjectContainer), + rightRimSampleTriggerSource = CreateTriggerSource(hitObjectContainer), }; } + protected virtual DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer) + => new DrumSampleTriggerSource(hitObjectContainer); + public bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index 6098db4f7a..939eba684b 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.UI { } - public void Play(HitType hitType) + public virtual void Play(HitType hitType) { TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; From beed390031bf015910cbb2da91a2d8c313e24a03 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 16:14:52 +0900 Subject: [PATCH 0527/2100] Add balance adjust to base implementation of `DrumSampleTriggerSource` --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 8 ++--- .../UI/DrumSamplePlayer.cs | 10 +++--- .../UI/DrumSampleTriggerSource.cs | 34 ++++++++++++++++++- .../UI/GameplaySampleTriggerSource.cs | 2 +- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index 5b690be780..9ceea14802 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -10,13 +10,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { public partial class ArgonDrumSamplePlayer : DrumSamplePlayer { - protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer) => - new ArgonDrumSampleTriggerSource(hitObjectContainer); + protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) => + new ArgonDrumSampleTriggerSource(hitObjectContainer, balance); public partial class ArgonDrumSampleTriggerSource : DrumSampleTriggerSource { - public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer) - : base(hitObjectContainer) + public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) + : base(hitObjectContainer, balance) { } diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index b60ad209ff..410072994f 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -25,14 +25,14 @@ namespace osu.Game.Rulesets.Taiko.UI InternalChildren = new Drawable[] { - leftRimSampleTriggerSource = CreateTriggerSource(hitObjectContainer), - leftCentreSampleTriggerSource = CreateTriggerSource(hitObjectContainer), - rightCentreSampleTriggerSource = CreateTriggerSource(hitObjectContainer), - rightRimSampleTriggerSource = CreateTriggerSource(hitObjectContainer), + leftRimSampleTriggerSource = CreateTriggerSource(hitObjectContainer, SampleBalance.Left), + leftCentreSampleTriggerSource = CreateTriggerSource(hitObjectContainer, SampleBalance.Left), + rightCentreSampleTriggerSource = CreateTriggerSource(hitObjectContainer, SampleBalance.Right), + rightRimSampleTriggerSource = CreateTriggerSource(hitObjectContainer, SampleBalance.Right), }; } - protected virtual DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer) + protected virtual DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) => new DrumSampleTriggerSource(hitObjectContainer); public bool OnPressed(KeyBindingPressEvent e) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index 939eba684b..50ff20f473 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -2,17 +2,35 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.UI; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.UI { public partial class DrumSampleTriggerSource : GameplaySampleTriggerSource { - public DrumSampleTriggerSource(HitObjectContainer hitObjectContainer) + private const double stereo_separation = 0.2; + + public DrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance = SampleBalance.Centre) : base(hitObjectContainer) { + switch (balance) + { + case SampleBalance.Left: + AudioContainer.Balance.Value = -stereo_separation; + break; + + case SampleBalance.Centre: + AudioContainer.Balance.Value = 0; + break; + + case SampleBalance.Right: + AudioContainer.Balance.Value = stereo_separation; + break; + } } public virtual void Play(HitType hitType) @@ -39,5 +57,19 @@ namespace osu.Game.Rulesets.Taiko.UI } public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead"); + + protected override void ApplySampleInfo(SkinnableSound hitSound, ISampleInfo[] samples) + { + base.ApplySampleInfo(hitSound, samples); + + hitSound.Balance.Value = -0.05 + RNG.NextDouble(0.1); + } + } + + public enum SampleBalance + { + Left, + Centre, + Right } } diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index efbf823058..58410ea14b 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.UI } } - private SkinnableSound getNextSample() + protected SkinnableSound GetNextSample() { SkinnableSound hitSound = hitSounds[nextHitSoundIndex]; From 27af07b74be9a67b82bb8823d86f6044ef9ec044 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 16:17:45 +0900 Subject: [PATCH 0528/2100] Add basic implementation of argon osu!taiko hitsounds (volume / flourish / strong) --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 70 +++++++++++++++++-- osu.Game/Audio/HitSampleInfo.cs | 6 ++ 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index 9ceea14802..b91e0f9901 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -1,10 +1,13 @@ // 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 osu.Framework.Allocation; using osu.Game.Audio; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Skinning.Argon { @@ -15,33 +18,90 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon public partial class ArgonDrumSampleTriggerSource : DrumSampleTriggerSource { + [Resolved] + private ISkinSource skinSource { get; set; } = null!; + public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) : base(hitObjectContainer, balance) { + // TODO: pool flourish sample } public override void Play(HitType hitType) { - // let the magic begin... - TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; if (hitObject == null) return; - var baseSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); + var baseSample = new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL)); + + // If the sample is provided by a legacy skin, we should not try and do anything special. + if (skinSource.FindProvider(s => s.GetSample(baseSample) != null) is LegacySkin) + { + base.Play(hitType); + return; + } + + // let the magic begin... if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject) { PlaySamples(new ISampleInfo[] { - hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH), + new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL), true), + // TODO: flourish should only play every time_between_flourishes. + new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_FLOURISH : string.Empty), true), baseSample }); } else { - PlaySamples(new ISampleInfo[] { baseSample }); + PlaySamples(new ISampleInfo[] { new VolumeAwareHitSampleInfo(baseSample) }); + } + } + + private class VolumeAwareHitSampleInfo : HitSampleInfo + { + public const int SAMPLE_VOLUME_THRESHOLD_HARD = 90; + public const int SAMPLE_VOLUME_THRESHOLD_MEDIUM = 60; + + public VolumeAwareHitSampleInfo(HitSampleInfo sampleInfo, bool isStrong = false) + : base(sampleInfo.Name, isStrong ? BANK_STRONG : getBank(sampleInfo.Bank, sampleInfo.Name, sampleInfo.Volume), sampleInfo.Suffix, sampleInfo.Volume) + { + } + + public override IEnumerable LookupNames + { + get + { + foreach (string name in base.LookupNames) + yield return name.Insert(name.LastIndexOf('/') + 1, "Argon/taiko-"); + } + } + + private static string getBank(string originalBank, string sampleName, int volume) + { + // So basically we're overwriting mapper's bank intentions here. + // The rationale is that most taiko beatmaps only use a single bank, but regularly adjust volume. + + switch (sampleName) + { + case HIT_NORMAL: + case HIT_CLAP: + { + if (volume >= SAMPLE_VOLUME_THRESHOLD_HARD) + return BANK_DRUM; + + if (volume >= SAMPLE_VOLUME_THRESHOLD_MEDIUM) + return BANK_NORMAL; + + return BANK_SOFT; + } + + default: + return originalBank; + } } } } diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 9573a9a4aa..24cb1730bf 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -24,6 +24,12 @@ namespace osu.Game.Audio public const string BANK_SOFT = @"soft"; public const string BANK_DRUM = @"drum"; + // new sample used exclusively by taiko for now. + public const string HIT_FLOURISH = "hitflourish"; + + // new bank used exclusively by taiko for now. + public const string BANK_STRONG = @"strong"; + /// /// All valid sample addition constants. /// From a9587fd1aa03f2916cf75d2987957b08238ed0de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 17:02:08 +0900 Subject: [PATCH 0529/2100] Move strong hit handling to `DrumSamplePlayer` and separte trigger sources --- .../Objects/Drawables/DrawableHit.cs | 6 +- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 3 +- .../UI/DrumSamplePlayer.cs | 106 +++++++++++++++--- .../UI/DrumSampleTriggerSource.cs | 1 + 4 files changed, 95 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 5b79151225..44225ab289 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -195,7 +195,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// The lenience for the second key press. /// This does not adjust by map difficulty in ScoreV2 yet. /// - private const double second_hit_window = 30; + public const double SECOND_HIT_WINDOW = 30; public StrongNestedHit() : this(null) @@ -223,12 +223,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!userTriggered) { - if (timeOffset - ParentHitObject.Result.TimeOffset > second_hit_window) + if (timeOffset - ParentHitObject.Result.TimeOffset > SECOND_HIT_WINDOW) ApplyResult(r => r.Type = r.Judgement.MinResult); return; } - if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= second_hit_window) + if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= SECOND_HIT_WINDOW) ApplyResult(r => r.Type = r.Judgement.MaxResult); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index b91e0f9901..a692260f10 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -11,7 +11,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Skinning.Argon { - public partial class ArgonDrumSamplePlayer : DrumSamplePlayer + internal partial class ArgonDrumSamplePlayer : DrumSamplePlayer { protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) => new ArgonDrumSampleTriggerSource(hitObjectContainer, balance); @@ -45,6 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon // let the magic begin... + // TODO: should we only play strong samples if the user correctly hits them? arguable. if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject) { PlaySamples(new ISampleInfo[] diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 410072994f..99449ecbda 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -7,28 +7,35 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Taiko.UI { - public partial class DrumSamplePlayer : CompositeDrawable, IKeyBindingHandler + internal partial class DrumSamplePlayer : CompositeDrawable, IKeyBindingHandler { - private DrumSampleTriggerSource leftRimSampleTriggerSource = null!; - private DrumSampleTriggerSource leftCentreSampleTriggerSource = null!; - private DrumSampleTriggerSource rightCentreSampleTriggerSource = null!; - private DrumSampleTriggerSource rightRimSampleTriggerSource = null!; + private DrumSampleTriggerSource leftCentreTrigger = null!; + private DrumSampleTriggerSource rightCentreTrigger = null!; + private DrumSampleTriggerSource leftRimTrigger = null!; + private DrumSampleTriggerSource rightRimTrigger = null!; + private DrumSampleTriggerSource strongCentreTrigger = null!; + private DrumSampleTriggerSource strongRimTrigger = null!; + + private double lastHitTime; + private TaikoAction? lastAction; [BackgroundDependencyLoader] private void load(DrawableRuleset drawableRuleset) { var hitObjectContainer = drawableRuleset.Playfield.HitObjectContainer; - InternalChildren = new Drawable[] { - leftRimSampleTriggerSource = CreateTriggerSource(hitObjectContainer, SampleBalance.Left), - leftCentreSampleTriggerSource = CreateTriggerSource(hitObjectContainer, SampleBalance.Left), - rightCentreSampleTriggerSource = CreateTriggerSource(hitObjectContainer, SampleBalance.Right), - rightRimSampleTriggerSource = CreateTriggerSource(hitObjectContainer, SampleBalance.Right), + leftCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Left), + rightCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Right), + leftRimTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Left), + rightRimTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Right), + strongCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Centre), + strongRimTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Centre) }; } @@ -37,28 +44,93 @@ namespace osu.Game.Rulesets.Taiko.UI public bool OnPressed(KeyBindingPressEvent e) { + HitType hitType; + + DrumSampleTriggerSource triggerSource; + + bool strong = checkStrongValidity(e.Action, lastAction, Time.Current - lastHitTime); + switch (e.Action) { - case TaikoAction.LeftRim: - leftRimSampleTriggerSource.Play(HitType.Rim); - break; - case TaikoAction.LeftCentre: - leftCentreSampleTriggerSource.Play(HitType.Centre); + hitType = HitType.Centre; + triggerSource = strong ? strongCentreTrigger : leftCentreTrigger; break; case TaikoAction.RightCentre: - rightCentreSampleTriggerSource.Play(HitType.Centre); + hitType = HitType.Centre; + triggerSource = strong ? strongCentreTrigger : rightCentreTrigger; + break; + + case TaikoAction.LeftRim: + hitType = HitType.Rim; + triggerSource = strong ? strongRimTrigger : leftRimTrigger; break; case TaikoAction.RightRim: - rightRimSampleTriggerSource.Play(HitType.Rim); + hitType = HitType.Rim; + triggerSource = strong ? strongRimTrigger : rightRimTrigger; break; + + default: + return false; } + if (strong && hitType == HitType.Centre) + flushCenterTriggerSources(); + + if (strong && hitType == HitType.Rim) + flushRimTriggerSources(); + + triggerSource.Play(hitType); + + lastHitTime = Time.Current; + lastAction = e.Action; + return false; } + private bool checkStrongValidity(TaikoAction newAction, TaikoAction? lastAction, double timeBetweenActions) + { + if (lastAction == null) + return false; + + if (timeBetweenActions > DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW) + return false; + + switch (newAction) + { + case TaikoAction.LeftCentre: + return lastAction == TaikoAction.RightCentre; + + case TaikoAction.RightCentre: + return lastAction == TaikoAction.LeftCentre; + + case TaikoAction.LeftRim: + return lastAction == TaikoAction.RightRim; + + case TaikoAction.RightRim: + return lastAction == TaikoAction.LeftRim; + + default: + return false; + } + } + + private void flushCenterTriggerSources() + { + leftCentreTrigger.StopAllPlayback(); + rightCentreTrigger.StopAllPlayback(); + strongCentreTrigger.StopAllPlayback(); + } + + private void flushRimTriggerSources() + { + leftRimTrigger.StopAllPlayback(); + rightRimTrigger.StopAllPlayback(); + strongRimTrigger.StopAllPlayback(); + } + public void OnReleased(KeyBindingReleaseEvent e) { } diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index 50ff20f473..a205d4fee1 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Taiko.UI var baseSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); + // TODO: should we only play strong samples if the user correctly hits them? arguable. if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject) { PlaySamples(new ISampleInfo[] From 010262c764cbcd58280f2b209a541476068f078f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 17:50:32 +0900 Subject: [PATCH 0530/2100] Change strong hit sample handling to be user input based, not hit object based --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 7 +++---- osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs | 2 +- osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index a692260f10..5996ba2d05 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon // TODO: pool flourish sample } - public override void Play(HitType hitType) + public override void Play(HitType hitType, bool strong) { TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; @@ -39,14 +39,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon // If the sample is provided by a legacy skin, we should not try and do anything special. if (skinSource.FindProvider(s => s.GetSample(baseSample) != null) is LegacySkin) { - base.Play(hitType); + base.Play(hitType, strong); return; } // let the magic begin... - // TODO: should we only play strong samples if the user correctly hits them? arguable. - if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject) + if (strong) { PlaySamples(new ISampleInfo[] { diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 99449ecbda..dba61a9b41 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Taiko.UI if (strong && hitType == HitType.Rim) flushRimTriggerSources(); - triggerSource.Play(hitType); + triggerSource.Play(hitType, strong); lastHitTime = Time.Current; lastAction = e.Action; diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index a205d4fee1..c87f95430f 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.UI } } - public virtual void Play(HitType hitType) + public virtual void Play(HitType hitType, bool strong) { TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Taiko.UI var baseSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); // TODO: should we only play strong samples if the user correctly hits them? arguable. - if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject) + if (strong) { PlaySamples(new ISampleInfo[] { From c72ebcfd539f8cca5b95dae78512d0f24ac7e4aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 18:09:50 +0900 Subject: [PATCH 0531/2100] Fix skin fallback not working as expected --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index 5996ba2d05..67d417c599 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -34,10 +34,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon if (hitObject == null) return; - var baseSample = new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL)); + var originalSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); // If the sample is provided by a legacy skin, we should not try and do anything special. - if (skinSource.FindProvider(s => s.GetSample(baseSample) != null) is LegacySkin) + if (skinSource.FindProvider(s => s.GetSample(originalSample) != null) is LegacySkinTransformer) { base.Play(hitType, strong); return; @@ -49,15 +49,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { PlaySamples(new ISampleInfo[] { - new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL), true), + new VolumeAwareHitSampleInfo(originalSample, true), // TODO: flourish should only play every time_between_flourishes. new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_FLOURISH : string.Empty), true), - baseSample + new VolumeAwareHitSampleInfo(originalSample) }); } else { - PlaySamples(new ISampleInfo[] { new VolumeAwareHitSampleInfo(baseSample) }); + PlaySamples(new ISampleInfo[] { new VolumeAwareHitSampleInfo(new VolumeAwareHitSampleInfo(originalSample)) }); } } From 16f1a7694d9397a9acac2acfd0c39f38ef438547 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 18:43:37 +0900 Subject: [PATCH 0532/2100] Add time-based flourish support --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 53 ++++++++++++++----- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index 67d417c599..72e5ac5b3d 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Game.Audio; using osu.Game.Rulesets.Taiko.Objects; @@ -18,12 +19,20 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon public partial class ArgonDrumSampleTriggerSource : DrumSampleTriggerSource { + private readonly HitObjectContainer hitObjectContainer; + [Resolved] private ISkinSource skinSource { get; set; } = null!; + /// + /// The minimum time to leave between flourishes that are added to strong rim hits. + /// + private const double time_between_flourishes = 2000; + public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) : base(hitObjectContainer, balance) { + this.hitObjectContainer = hitObjectContainer; // TODO: pool flourish sample } @@ -44,21 +53,41 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon } // let the magic begin... + var samplesToPlay = new List { new VolumeAwareHitSampleInfo(originalSample, strong) }; - if (strong) + if (strong && hitType == HitType.Rim && canPlayFlourish(hitObject)) + samplesToPlay.Add(new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(HitSampleInfo.HIT_FLOURISH), true)); + + PlaySamples(samplesToPlay.ToArray()); + } + + private bool canPlayFlourish(TaikoHitObject hitObject) + { + double? lastFlourish = null; + + // TODO: check on nested strong hits. we're not accounting for them here yet. + + var hitObjects = hitObjectContainer.AliveObjects + .Reverse() + .Select(d => d.HitObject) + .OfType() + .Where(h => h.IsStrong && h.Type == HitType.Rim); + + // Add an additional 'flourish' sample to strong rim hits (that are at least `time_between_flourishes` apart). + // This is applied to hitobjects in reverse order, as to sound more musically coherent by biasing towards to + // end of groups/combos of strong rim hits instead of the start. + foreach (var h in hitObjects) { - PlaySamples(new ISampleInfo[] - { - new VolumeAwareHitSampleInfo(originalSample, true), - // TODO: flourish should only play every time_between_flourishes. - new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_FLOURISH : string.Empty), true), - new VolumeAwareHitSampleInfo(originalSample) - }); - } - else - { - PlaySamples(new ISampleInfo[] { new VolumeAwareHitSampleInfo(new VolumeAwareHitSampleInfo(originalSample)) }); + bool canFlourish = lastFlourish == null || lastFlourish - h.StartTime >= time_between_flourishes; + + if (canFlourish) + lastFlourish = h.StartTime; + + if (h == hitObject) + return canFlourish; } + + return false; } private class VolumeAwareHitSampleInfo : HitSampleInfo From f08690883118dfaa5bf78af204178d17a007f771 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Jul 2023 13:49:14 +0900 Subject: [PATCH 0533/2100] Don't attempt to play drum samples when rewinding --- osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index dba61a9b41..4f727e8c90 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -44,6 +44,9 @@ namespace osu.Game.Rulesets.Taiko.UI public bool OnPressed(KeyBindingPressEvent e) { + if (Time.Elapsed < 0) + return false; + HitType hitType; DrumSampleTriggerSource triggerSource; From 759cd5aec79686df8553efc5582524b27d0044c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Jul 2023 14:05:40 +0900 Subject: [PATCH 0534/2100] Warm up pool with argon-specific drum samples --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 14 ++++++++++---- osu.Game/Skinning/IPooledSampleProvider.cs | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index 72e5ac5b3d..91393d99fe 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -14,6 +14,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { internal partial class ArgonDrumSamplePlayer : DrumSamplePlayer { + [BackgroundDependencyLoader] + private void load(IPooledSampleProvider sampleProvider) + { + // Warm up pools for non-standard samples. + sampleProvider.GetPooledSample(new ArgonDrumSampleTriggerSource.VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_NORMAL), true)); + sampleProvider.GetPooledSample(new ArgonDrumSampleTriggerSource.VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_CLAP), true)); + sampleProvider.GetPooledSample(new ArgonDrumSampleTriggerSource.VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_FLOURISH), true)); + } + protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) => new ArgonDrumSampleTriggerSource(hitObjectContainer, balance); @@ -33,7 +42,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon : base(hitObjectContainer, balance) { this.hitObjectContainer = hitObjectContainer; - // TODO: pool flourish sample } public override void Play(HitType hitType, bool strong) @@ -65,8 +73,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { double? lastFlourish = null; - // TODO: check on nested strong hits. we're not accounting for them here yet. - var hitObjects = hitObjectContainer.AliveObjects .Reverse() .Select(d => d.HitObject) @@ -90,7 +96,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon return false; } - private class VolumeAwareHitSampleInfo : HitSampleInfo + public class VolumeAwareHitSampleInfo : HitSampleInfo { public const int SAMPLE_VOLUME_THRESHOLD_HARD = 90; public const int SAMPLE_VOLUME_THRESHOLD_MEDIUM = 60; diff --git a/osu.Game/Skinning/IPooledSampleProvider.cs b/osu.Game/Skinning/IPooledSampleProvider.cs index 3ea299f5e2..0e57050c4d 100644 --- a/osu.Game/Skinning/IPooledSampleProvider.cs +++ b/osu.Game/Skinning/IPooledSampleProvider.cs @@ -8,7 +8,7 @@ namespace osu.Game.Skinning /// /// Provides pooled samples to be used by s. /// - internal interface IPooledSampleProvider + public interface IPooledSampleProvider { /// /// Retrieves a from a pool. From 561fff801a5c33d28f67b08ede925f17fdc5b8d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Jul 2023 13:29:44 +0900 Subject: [PATCH 0535/2100] Consume nested object states in `HitObjectLifetimeEntry` --- osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index 58410ea14b..18d412ab44 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.UI return getAllNested(mostValidObject.HitObject).OrderBy(h => h.GetEndTime()).SkipWhile(h => h.GetEndTime() <= getReferenceTime()).FirstOrDefault() ?? mostValidObject.HitObject; } - private bool isAlreadyHit(HitObjectLifetimeEntry h) => h.Result?.HasResult == true; + private bool isAlreadyHit(HitObjectLifetimeEntry h) => h.AllJudged; private bool isCloseEnoughToCurrentTime(HitObject h) => getReferenceTime() >= h.StartTime - h.HitWindows.WindowFor(HitResult.Miss) * 2; private double getReferenceTime() => gameplayClock?.CurrentTime ?? Clock.CurrentTime; From 8413247773ba495307a15085ec2302fce690e901 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 13:10:02 +0900 Subject: [PATCH 0536/2100] Fix failing test --- .../TestSceneDrumSampleTriggerSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs index 4133b96d42..06e88643de 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs @@ -372,7 +372,7 @@ namespace osu.Game.Rulesets.Taiko.Tests private void checkSamples(HitType hitType, string expectedSamplesCsv, string expectedBank) { - AddStep($"hit {hitType}", () => triggerSource.Play(hitType)); + AddStep($"hit {hitType}", () => triggerSource.Play(hitType, false)); AddAssert($"last played sample is {expectedSamplesCsv}", () => string.Join(',', triggerSource.LastPlayedSamples!.OfType().Select(s => s.Name)), () => Is.EqualTo(expectedSamplesCsv)); AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType().First().Bank, () => Is.EqualTo(expectedBank)); From 4364736ccd6f8a0d06e654dd9fee061910997643 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 18:02:03 +0900 Subject: [PATCH 0537/2100] Fix fallback for `Judged` to be more correct Without this change, when the `Judged` value is checked on an `HitObjectLifetimeEntry` it would return `true` if a `DrawableHitObject` has not yet been associated with the entry. Which is completely wrong. Of note, the usage in `DrawableHitObject` will have never fallen through to this incorrect value as they always have a result populated: https://github.com/ppy/osu/blob/f26f001e1d01ca6bb53225da7bf06c0ad21153c5/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs#L721-L726 --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 4 ++-- osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 08dcf91e52..e4d8eb2335 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -112,12 +112,12 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Whether this has been judged. /// Note: This does NOT include nested hitobjects. /// - public bool Judged => Entry?.Judged ?? true; + public bool Judged => Entry?.Judged ?? false; /// /// Whether this and all of its nested s have been judged. /// - public bool AllJudged => Entry?.AllJudged ?? true; + public bool AllJudged => Entry?.AllJudged ?? false; /// /// The relative X position of this hit object for sample playback balance adjustment. diff --git a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs index 1d99e7440f..4450f026b4 100644 --- a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs +++ b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Objects /// Whether has been judged. /// Note: This does NOT include nested hitobjects. /// - public bool Judged => Result?.HasResult ?? true; + public bool Judged => Result?.HasResult ?? false; /// /// Whether and all of its nested objects have been judged. From 168b6c70a98ed59b56d52572bcfe29abcf1ab6cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 15:12:40 +0900 Subject: [PATCH 0538/2100] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index b4d8dd513f..83d4bcb336 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From 7355a6a66bf695a0addd9cd2a1f5a310edfc284f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 18:20:25 +0900 Subject: [PATCH 0539/2100] Add test coverage of `Judged` state --- .../Gameplay/TestSceneDrawableHitObject.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs index 04fc4cafbd..10dbede2e0 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; @@ -80,7 +81,9 @@ namespace osu.Game.Tests.Gameplay { TestLifetimeEntry entry = null; AddStep("Create entry", () => entry = new TestLifetimeEntry(new HitObject()) { LifetimeStart = 1 }); + assertJudged(() => entry, false); AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty())); + assertJudged(() => entry, false); AddAssert("Lifetime is updated", () => entry.LifetimeStart == -TestLifetimeEntry.INITIAL_LIFETIME_OFFSET); TestDrawableHitObject dho = null; @@ -91,6 +94,7 @@ namespace osu.Game.Tests.Gameplay }); AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty())); AddAssert("Lifetime is correct", () => dho.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY && entry.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY); + assertJudged(() => entry, false); } [Test] @@ -138,6 +142,29 @@ namespace osu.Game.Tests.Gameplay AddAssert("DHO state is correct", () => dho.State.Value == ArmedState.Miss); } + [Test] + public void TestJudgedStateThroughLifetime() + { + TestDrawableHitObject dho = null; + HitObjectLifetimeEntry lifetimeEntry = null; + + AddStep("Create lifetime entry", () => lifetimeEntry = new HitObjectLifetimeEntry(new HitObject { StartTime = Time.Current })); + + assertJudged(() => lifetimeEntry, false); + + AddStep("Create DHO and apply entry", () => + { + Child = dho = new TestDrawableHitObject(); + dho.Apply(lifetimeEntry); + }); + + assertJudged(() => lifetimeEntry, false); + + AddStep("Apply result", () => dho.MissForcefully()); + + assertJudged(() => lifetimeEntry, true); + } + [Test] public void TestResultSetBeforeLoadComplete() { @@ -154,15 +181,20 @@ namespace osu.Game.Tests.Gameplay } }; }); + assertJudged(() => lifetimeEntry, true); AddStep("Create DHO and apply entry", () => { dho = new TestDrawableHitObject(); dho.Apply(lifetimeEntry); Child = dho; }); + assertJudged(() => lifetimeEntry, true); AddAssert("DHO state is correct", () => dho.State.Value, () => Is.EqualTo(ArmedState.Hit)); } + private void assertJudged(Func entry, bool val) => + AddAssert(val ? "Is judged" : "Not judged", () => entry().Judged, () => Is.EqualTo(val)); + private partial class TestDrawableHitObject : DrawableHitObject { public const double INITIAL_LIFETIME_OFFSET = 100; From 51b0d18c04e458b7785db34dfe4613e682bf7396 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 17:41:51 +0900 Subject: [PATCH 0540/2100] Fix weird test assertion output --- .../Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index 6701871e8d..59c9c4587a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -197,7 +197,7 @@ namespace osu.Game.Tests.Visual.Gameplay } private void checkValidObjectIndex(int index) => - AddAssert($"check valid object is {index}", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[index])); + AddAssert($"check object at index {index} is correct", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[index])); private DrawableHitObject? getNextAliveObject() => Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault(); From 289f916cd7e37bcb6164a5412ea86f7db25cae94 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 18:36:41 +0900 Subject: [PATCH 0541/2100] Remove outdated TODO --- osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index c87f95430f..1de16c2294 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -42,7 +42,6 @@ namespace osu.Game.Rulesets.Taiko.UI var baseSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); - // TODO: should we only play strong samples if the user correctly hits them? arguable. if (strong) { PlaySamples(new ISampleInfo[] From 8f6b06fe4004bfc3be183080fee05d0e802b05eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 18:38:19 +0900 Subject: [PATCH 0542/2100] Update test assumptions --- .../TestSceneDrumSampleTriggerSource.cs | 104 +++++++++--------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs index 06e88643de..ced2e4b98c 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs @@ -72,13 +72,13 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); } [Test] @@ -100,13 +100,13 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); seekTo(200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); } [Test] @@ -145,23 +145,23 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first)); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); seekTo(120); AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first)); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); seekTo(480); AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second)); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); seekTo(700); AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second)); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); } [Test] @@ -184,13 +184,13 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); - checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); seekTo(200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); - checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); } [Test] @@ -213,18 +213,18 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(600); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); } [Test] @@ -247,18 +247,18 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); seekTo(600); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); seekTo(1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); } [Test] @@ -282,18 +282,18 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); - checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); seekTo(600); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); - checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); seekTo(1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); - checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); } [Test] @@ -319,18 +319,18 @@ namespace osu.Game.Rulesets.Taiko.Tests // This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits. // But for sample playback purposes they can be ignored as noise. AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(600); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(1200); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); } [Test] @@ -356,23 +356,23 @@ namespace osu.Game.Rulesets.Taiko.Tests // This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits. // But for sample playback purposes they can be ignored as noise. AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); seekTo(600); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); seekTo(1200); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); } - private void checkSamples(HitType hitType, string expectedSamplesCsv, string expectedBank) + private void checkSamples(HitType hitType, bool strong, string expectedSamplesCsv, string expectedBank) { - AddStep($"hit {hitType}", () => triggerSource.Play(hitType, false)); + AddStep($"hit {hitType}", () => triggerSource.Play(hitType, strong)); AddAssert($"last played sample is {expectedSamplesCsv}", () => string.Join(',', triggerSource.LastPlayedSamples!.OfType().Select(s => s.Name)), () => Is.EqualTo(expectedSamplesCsv)); AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType().First().Bank, () => Is.EqualTo(expectedBank)); From 8f61f5e4c6c60d0c8933756929f16efd6728f279 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 18:41:32 +0900 Subject: [PATCH 0543/2100] Cache `Playfield` for the sake of tests I'm open to an alternative. Name it. --- osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs | 4 ++-- osu.Game/Rulesets/UI/Playfield.cs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 4f727e8c90..2797492b0a 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -25,9 +25,9 @@ namespace osu.Game.Rulesets.Taiko.UI private TaikoAction? lastAction; [BackgroundDependencyLoader] - private void load(DrawableRuleset drawableRuleset) + private void load(Playfield playfield) { - var hitObjectContainer = drawableRuleset.Playfield.HitObjectContainer; + var hitObjectContainer = playfield.HitObjectContainer; InternalChildren = new Drawable[] { leftCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Left), diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 6016a53918..3f263aba63 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -28,6 +28,7 @@ namespace osu.Game.Rulesets.UI { [Cached(typeof(IPooledHitObjectProvider))] [Cached(typeof(IPooledSampleProvider))] + [Cached] public abstract partial class Playfield : CompositeDrawable, IPooledHitObjectProvider, IPooledSampleProvider { /// From 00c68cad532d5898f5d6135d6df202db00caf9f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 19:47:44 +0900 Subject: [PATCH 0544/2100] Fix new scoring related properties not storing to realm due to `internal` spec --- osu.Game/Scoring/ScoreInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index eddd1bb80a..ea9007f4dc 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -64,7 +64,7 @@ namespace osu.Game.Scoring /// /// This may not match the version stored in the replay files. /// - internal int TotalScoreVersion { get; set; } = LegacyScoreEncoder.LATEST_VERSION; + public int TotalScoreVersion { get; set; } = LegacyScoreEncoder.LATEST_VERSION; /// /// Used to preserve the total score for legacy scores. @@ -72,7 +72,7 @@ namespace osu.Game.Scoring /// /// Not populated if is false. /// - internal long? LegacyTotalScore { get; set; } + public long? LegacyTotalScore { get; set; } public int MaxCombo { get; set; } From fbab5acac100211f0cffe6bdf7182eff6297baf1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 00:46:09 +0900 Subject: [PATCH 0545/2100] Remove not-yet-implemented settings group comments --- osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index b6b385e262..2fd2ed30f6 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -37,8 +37,6 @@ namespace osu.Game.Screens.Play.HUD Spacing = new Vector2(0, 20), Children = new PlayerSettingsGroup[] { - //CollectionSettings = new CollectionSettings(), - //DiscussionSettings = new DiscussionSettings(), PlaybackSettings = new PlaybackSettings { Expanded = { Value = false } }, VisualSettings = new VisualSettings { Expanded = { Value = false } }, new AudioSettings { Expanded = { Value = false } } From 95a9b532dfc8feff57e524a2361798a6968ff2e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 00:46:28 +0900 Subject: [PATCH 0546/2100] Remember state of replay settings visibility --- osu.Game/Configuration/OsuConfigManager.cs | 4 +- .../Localisation/GameplaySettingsStrings.cs | 9 +++- .../Settings/Sections/Gameplay/HUDSettings.cs | 14 ++++-- .../Screens/Play/HUD/PlayerSettingsOverlay.cs | 25 ---------- osu.Game/Screens/Play/HUDOverlay.cs | 47 ++++++++++--------- 5 files changed, 45 insertions(+), 54 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index ba555a7926..edcbb94368 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -129,6 +129,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true); SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true); SetDefault(OsuSetting.KeyOverlay, false); + SetDefault(OsuSetting.ReplaySettingsOverlay, true); SetDefault(OsuSetting.GameplayLeaderboard, true); SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true); @@ -382,6 +383,7 @@ namespace osu.Game.Configuration SafeAreaConsiderations, ComboColourNormalisationAmount, ProfileCoverExpanded, - EditorLimitedDistanceSnap + EditorLimitedDistanceSnap, + ReplaySettingsOverlay } } diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs index 40f39d927d..f52f6abb89 100644 --- a/osu.Game/Localisation/GameplaySettingsStrings.cs +++ b/osu.Game/Localisation/GameplaySettingsStrings.cs @@ -65,10 +65,15 @@ namespace osu.Game.Localisation public static LocalisableString HUDVisibilityMode => new TranslatableString(getKey(@"hud_visibility_mode"), @"HUD overlay visibility mode"); /// - /// "Show health display even when you can't fail" + /// "Show health display even when you can't fail" /// public static LocalisableString ShowHealthDisplayWhenCantFail => new TranslatableString(getKey(@"show_health_display_when_cant_fail"), @"Show health display even when you can't fail"); + /// + /// "Show replay settings overlay" + /// + public static LocalisableString ShowReplaySettingsOverlay => new TranslatableString(getKey(@"show_replay_settings_overlay"), @"Show replay settings overlay"); + /// /// "Fade playfield to red when health is low" /// @@ -134,6 +139,6 @@ namespace osu.Game.Localisation /// public static LocalisableString ClassicScoreDisplay => new TranslatableString(getKey(@"classic_score_display"), @"Classic"); - private static string getKey(string key) => $"{prefix}:{key}"; + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs index e7c83159cd..3e67b2f103 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs @@ -25,10 +25,9 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay }, new SettingsCheckbox { - ClassicDefault = false, - LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail, - Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), - Keywords = new[] { "hp", "bar" } + LabelText = GameplaySettingsStrings.ShowReplaySettingsOverlay, + Current = config.GetBindable(OsuSetting.ReplaySettingsOverlay), + Keywords = new[] { "hide" }, }, new SettingsCheckbox { @@ -41,6 +40,13 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = GameplaySettingsStrings.AlwaysShowGameplayLeaderboard, Current = config.GetBindable(OsuSetting.GameplayLeaderboard), }, + new SettingsCheckbox + { + ClassicDefault = false, + LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail, + Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), + Keywords = new[] { "hp", "bar" } + }, }; } } diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index 2fd2ed30f6..dbb0456cd0 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -3,10 +3,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; using osuTK; using osu.Game.Screens.Play.PlayerSettings; -using osuTK.Input; namespace osu.Game.Screens.Play.HUD { @@ -14,16 +12,12 @@ namespace osu.Game.Screens.Play.HUD { private const int fade_duration = 200; - public bool ReplayLoaded; - public readonly PlaybackSettings PlaybackSettings; public readonly VisualSettings VisualSettings; public PlayerSettingsOverlay() { - AlwaysPresent = true; - Anchor = Anchor.TopRight; Origin = Anchor.TopRight; AutoSizeAxes = Axes.Both; @@ -46,24 +40,5 @@ namespace osu.Game.Screens.Play.HUD protected override void PopIn() => this.FadeIn(fade_duration); protected override void PopOut() => this.FadeOut(fade_duration); - - // We want to handle keyboard inputs all the time in order to trigger ToggleVisibility() when not visible - public override bool PropagateNonPositionalInputSubTree => true; - - protected override bool OnKeyDown(KeyDownEvent e) - { - if (e.Repeat) return false; - - if (e.ControlPressed) - { - if (e.Key == Key.H && ReplayLoaded) - { - ToggleVisibility(); - return true; - } - } - - return base.OnKeyDown(e); - } } } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index f0a2975958..55843ec17c 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -28,6 +28,7 @@ using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osu.Game.Screens.Play.HUD.JudgementCounter; using osu.Game.Skinning; using osuTK; +using MarginPadding = osu.Framework.Graphics.MarginPadding; namespace osu.Game.Screens.Play { @@ -78,6 +79,7 @@ namespace osu.Game.Screens.Play public Bindable ShowHud { get; } = new BindableBool(); private Bindable configVisibilityMode; + private Bindable configSettingsOverlay; private readonly BindableBool replayLoaded = new BindableBool(); @@ -178,6 +180,7 @@ namespace osu.Game.Screens.Play ModDisplay.Current.Value = mods; configVisibilityMode = config.GetBindable(OsuSetting.HUDVisibilityMode); + configSettingsOverlay = config.GetBindable(OsuSetting.ReplaySettingsOverlay); if (configVisibilityMode.Value == HUDVisibilityMode.Never && !hasShownNotificationOnce) { @@ -204,9 +207,24 @@ namespace osu.Game.Screens.Play holdingForHUD.BindValueChanged(_ => updateVisibility()); IsPlaying.BindValueChanged(_ => updateVisibility()); - configVisibilityMode.BindValueChanged(_ => updateVisibility(), true); + configVisibilityMode.BindValueChanged(_ => updateVisibility()); + configSettingsOverlay.BindValueChanged(_ => updateVisibility()); - replayLoaded.BindValueChanged(replayLoadedValueChanged, true); + replayLoaded.BindValueChanged(e => + { + if (e.NewValue) + { + ModDisplay.FadeIn(200); + InputCountController.Margin = new MarginPadding(10) { Bottom = 30 }; + } + else + { + ModDisplay.Delay(2000).FadeOut(200); + InputCountController.Margin = new MarginPadding(10); + } + + updateVisibility(); + }, true); } protected override void Update() @@ -280,6 +298,11 @@ namespace osu.Game.Screens.Play return; } + if (configSettingsOverlay.Value && replayLoaded.Value) + PlayerSettingsOverlay.Show(); + else + PlayerSettingsOverlay.Hide(); + switch (configVisibilityMode.Value) { case HUDVisibilityMode.Never: @@ -297,26 +320,6 @@ namespace osu.Game.Screens.Play } } - private void replayLoadedValueChanged(ValueChangedEvent e) - { - PlayerSettingsOverlay.ReplayLoaded = e.NewValue; - - if (e.NewValue) - { - PlayerSettingsOverlay.Show(); - ModDisplay.FadeIn(200); - InputCountController.Margin = new MarginPadding(10) { Bottom = 30 }; - } - else - { - PlayerSettingsOverlay.Hide(); - ModDisplay.Delay(2000).FadeOut(200); - InputCountController.Margin = new MarginPadding(10); - } - - updateVisibility(); - } - protected virtual void BindDrawableRuleset(DrawableRuleset drawableRuleset) { if (drawableRuleset is ICanAttachHUDPieces attachTarget) From 929189530529ec3129e84b838f84bdf6e07441ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 01:00:41 +0900 Subject: [PATCH 0547/2100] Make key for toggling replay settings customisable --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ++++ osu.Game/Localisation/GlobalActionKeyBindingStrings.cs | 5 +++++ osu.Game/Screens/Play/HUDOverlay.cs | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 64268c73d0..c2d08ffff8 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -116,6 +116,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.F3 }, GlobalAction.DecreaseScrollSpeed), new KeyBinding(new[] { InputKey.F4 }, GlobalAction.IncreaseScrollSpeed), new KeyBinding(new[] { InputKey.Shift, InputKey.Tab }, GlobalAction.ToggleInGameInterface), + new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.ToggleReplaySettings), new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), new KeyBinding(InputKey.Tab, GlobalAction.ToggleChatFocus), @@ -374,5 +375,8 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ExportReplay))] ExportReplay, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleReplaySettings))] + ToggleReplaySettings, } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 9e53b23180..f93d86225c 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -324,6 +324,11 @@ namespace osu.Game.Localisation /// public static LocalisableString ToggleChatFocus => new TranslatableString(getKey(@"toggle_chat_focus"), @"Toggle chat focus"); + /// + /// "Toggle replay settings" + /// + public static LocalisableString ToggleReplaySettings => new TranslatableString(getKey(@"toggle_replay_settings"), @"Toggle replay settings"); + /// /// "Save replay" /// diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 55843ec17c..d8d4daf143 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -357,6 +357,10 @@ namespace osu.Game.Screens.Play switch (e.Action) { + case GlobalAction.ToggleReplaySettings: + configSettingsOverlay.Value = !configSettingsOverlay.Value; + return true; + case GlobalAction.HoldForHUD: holdingForHUD.Value = true; return true; From cdb8a56df40b83c40f243d2dab9df2a531139084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Jul 2023 22:41:20 +0200 Subject: [PATCH 0548/2100] Remove weird aliased using Doesn't appear to be required. --- osu.Game/Screens/Play/HUDOverlay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index d8d4daf143..d11171e3fe 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -28,7 +28,6 @@ using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osu.Game.Screens.Play.HUD.JudgementCounter; using osu.Game.Skinning; using osuTK; -using MarginPadding = osu.Framework.Graphics.MarginPadding; namespace osu.Game.Screens.Play { From 1938bdbf9d42a69d6a718b8022fb886eaf78a554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Jul 2023 22:45:10 +0200 Subject: [PATCH 0549/2100] Move replay settings toggle to replay key bindings section --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index c2d08ffff8..01c454e3f9 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -116,7 +116,6 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.F3 }, GlobalAction.DecreaseScrollSpeed), new KeyBinding(new[] { InputKey.F4 }, GlobalAction.IncreaseScrollSpeed), new KeyBinding(new[] { InputKey.Shift, InputKey.Tab }, GlobalAction.ToggleInGameInterface), - new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.ToggleReplaySettings), new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), new KeyBinding(InputKey.Tab, GlobalAction.ToggleChatFocus), @@ -130,6 +129,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.MouseMiddle, GlobalAction.TogglePauseReplay), new KeyBinding(InputKey.Left, GlobalAction.SeekReplayBackward), new KeyBinding(InputKey.Right, GlobalAction.SeekReplayForward), + new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.ToggleReplaySettings), }; public IEnumerable SongSelectKeyBindings => new[] From 170bc5bfcec23252a836c31829aa971af778ce84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 12:25:15 +0900 Subject: [PATCH 0550/2100] Add support for skinnable "retry" sound --- osu.Game/Screens/Play/Player.cs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b979fc2740..2cb7748a15 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -11,8 +11,6 @@ using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; @@ -114,7 +112,7 @@ namespace osu.Game.Screens.Play private Ruleset ruleset; - private Sample sampleRestart; + private SkinnableSound sampleRestart; public BreakOverlay BreakOverlay; @@ -195,7 +193,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader(true)] - private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game, CancellationToken cancellationToken) + private void load(OsuConfigManager config, OsuGameBase game, CancellationToken cancellationToken) { var gameplayMods = Mods.Value.Select(m => m.DeepClone()).ToArray(); @@ -213,8 +211,6 @@ namespace osu.Game.Screens.Play if (playableBeatmap == null) return; - sampleRestart = audio.Samples.Get(@"Gameplay/restart"); - mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); if (game != null) @@ -295,15 +291,19 @@ namespace osu.Game.Screens.Play if (Configuration.AllowRestart) { - rulesetSkinProvider.Add(new HotkeyRetryOverlay + rulesetSkinProvider.AddRange(new Drawable[] { - Action = () => + new HotkeyRetryOverlay { - if (!this.IsCurrentScreen()) return; + Action = () => + { + if (!this.IsCurrentScreen()) return; - fadeOut(true); - Restart(true); + fadeOut(true); + Restart(true); + }, }, + sampleRestart = new SkinnableSound(new SampleInfo(@"Gameplay/restart", @"pause-retry-click")) }); } From b679ab88a161cca1331c5bf939927c498af44df0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 12:29:03 +0900 Subject: [PATCH 0551/2100] Avoid attempting to process missing statistics on scores without linked beatmaps --- osu.Game/BackgroundBeatmapProcessor.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 9fe3a41b03..9c140bdda9 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -163,8 +163,12 @@ namespace osu.Game { foreach (var score in r.All()) { - if (score.Statistics.Sum(kvp => kvp.Value) > 0 && score.MaximumStatistics.Sum(kvp => kvp.Value) == 0) + if (score.BeatmapInfo != null + && score.Statistics.Sum(kvp => kvp.Value) > 0 + && score.MaximumStatistics.Sum(kvp => kvp.Value) == 0) + { scoreIds.Add(score.ID); + } } }); From a98a36872e286da460737d92e0658713ba690292 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 13:37:42 +0900 Subject: [PATCH 0552/2100] Bring realm library up-to-date --- .../Beatmaps/WorkingBeatmapManagerTest.cs | 6 ++--- .../BackgroundBeatmapProcessorTests.cs | 14 +++++------ .../Database/BeatmapImporterUpdateTests.cs | 14 +++++------ osu.Game.Tests/Database/GeneralUsageTests.cs | 2 +- osu.Game.Tests/Database/RealmLiveTests.cs | 2 +- .../RealmSubscriptionRegistrationTests.cs | 7 +++--- osu.Game.Tests/Database/RulesetStoreTests.cs | 10 ++++---- .../TestScenePlayerLocalScoreImport.cs | 2 +- .../UserInterface/TestSceneModPresetColumn.cs | 6 ++--- .../TestSceneModSelectOverlay.cs | 4 ++-- osu.Game/BackgroundBeatmapProcessor.cs | 6 ++--- osu.Game/Beatmaps/BeatmapImporter.cs | 2 +- osu.Game/Beatmaps/BeatmapManager.cs | 8 +++---- osu.Game/Beatmaps/BeatmapUpdater.cs | 2 +- osu.Game/Collections/CollectionDropdown.cs | 2 +- .../Collections/DrawableCollectionList.cs | 2 +- .../Collections/DrawableCollectionListItem.cs | 2 +- osu.Game/Database/EmptyRealmSet.cs | 4 ++-- osu.Game/Database/ModelManager.cs | 24 ++++++++++--------- osu.Game/Database/RealmAccess.cs | 14 +++++------ osu.Game/Database/RealmLive.cs | 6 ++--- osu.Game/Database/RealmObjectExtensions.cs | 8 +++---- .../Bindings/DatabasedKeyBindingContainer.cs | 2 +- osu.Game/Online/BeatmapDownloadTracker.cs | 2 +- .../OnlinePlayBeatmapAvailabilityTracker.cs | 2 +- osu.Game/Online/ScoreDownloadTracker.cs | 2 +- .../Overlays/FirstRunSetup/ScreenBeatmaps.cs | 2 +- osu.Game/Overlays/Mods/AddPresetPopover.cs | 2 +- osu.Game/Overlays/Mods/ModPresetColumn.cs | 2 +- osu.Game/Overlays/Music/PlaylistOverlay.cs | 2 +- .../Overlays/Settings/Sections/SkinSection.cs | 2 +- osu.Game/Scoring/ScoreImporter.cs | 4 ++-- osu.Game/Screens/Select/BeatmapCarousel.cs | 8 +++---- .../Screens/Select/Carousel/TopLocalRank.cs | 2 +- .../Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 2 +- osu.Game/Skinning/RealmBackedResourceStore.cs | 2 +- osu.Game/Skinning/SkinImporter.cs | 6 ++--- osu.Game/osu.Game.csproj | 2 +- 39 files changed, 97 insertions(+), 96 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs b/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs index 89b8c8927d..237fe758b5 100644 --- a/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs +++ b/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestCachedRetrievalWithFiles() => AddStep("run test", () => { - var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID).Detach()); + var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID)!.Detach()); Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0)); @@ -90,7 +90,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestForcedRefetchRetrievalWithFiles() => AddStep("run test", () => { - var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID).Detach()); + var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID)!.Detach()); Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0)); @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestSavePreservesCollections() => AddStep("run test", () => { - var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID).Detach()); + var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID)!.Detach()); var working = beatmaps.GetWorkingBeatmap(beatmap); diff --git a/osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs b/osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs index ddb60606ec..c876316be4 100644 --- a/osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Database { return Realm.Run(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); }); }); @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Database { Realm.Write(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; foreach (var b in beatmapSetInfo.Beatmaps) b.StarRating = -1; }); @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Database { return Realm.Run(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); }); }); @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Database { return Realm.Run(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); }); }); @@ -90,7 +90,7 @@ namespace osu.Game.Tests.Database { Realm.Write(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; foreach (var b in beatmapSetInfo.Beatmaps) b.StarRating = -1; }); @@ -107,7 +107,7 @@ namespace osu.Game.Tests.Database { return Realm.Run(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; return beatmapSetInfo.Beatmaps.All(b => b.StarRating == -1); }); }); @@ -118,7 +118,7 @@ namespace osu.Game.Tests.Database { return Realm.Run(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); }); }); diff --git a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs index 83cb54df3f..437a8c75e2 100644 --- a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs @@ -323,7 +323,7 @@ namespace osu.Game.Tests.Database var beatmapInfo = s.Beatmaps.First(b => b.File?.Filename != removedFilename); scoreTargetBeatmapHash = beatmapInfo.Hash; - s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); + s.Realm!.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); }); realm.Run(r => r.Refresh()); @@ -372,7 +372,7 @@ namespace osu.Game.Tests.Database scoreTargetBeatmapHash = beatmapInfo.Hash; - s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); + s.Realm!.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); }); // locally modify beatmap @@ -435,7 +435,7 @@ namespace osu.Game.Tests.Database { var beatmapInfo = s.Beatmaps.Last(); scoreTargetFilename = beatmapInfo.File?.Filename; - s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); + s.Realm!.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); }); realm.Run(r => r.Refresh()); @@ -528,7 +528,7 @@ namespace osu.Game.Tests.Database importBeforeUpdate.PerformWrite(s => { - var beatmapCollection = s.Realm.Add(new BeatmapCollection("test collection")); + var beatmapCollection = s.Realm!.Add(new BeatmapCollection("test collection")); beatmapsToAddToCollection = s.Beatmaps.Count - (allOriginalBeatmapsInCollection ? 0 : 1); for (int i = 0; i < beatmapsToAddToCollection; i++) @@ -543,7 +543,7 @@ namespace osu.Game.Tests.Database importAfterUpdate.PerformRead(updated => { - updated.Realm.Refresh(); + updated.Realm!.Refresh(); string[] hashes = updated.Realm.All().Single().BeatmapMD5Hashes.ToArray(); @@ -593,7 +593,7 @@ namespace osu.Game.Tests.Database importBeforeUpdate.PerformWrite(s => { - var beatmapCollection = s.Realm.Add(new BeatmapCollection("test collection")); + var beatmapCollection = s.Realm!.Add(new BeatmapCollection("test collection")); originalHash = s.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash; beatmapCollection.BeatmapMD5Hashes.Add(originalHash); @@ -607,7 +607,7 @@ namespace osu.Game.Tests.Database importAfterUpdate.PerformRead(updated => { - updated.Realm.Refresh(); + updated.Realm!.Refresh(); string[] hashes = updated.Realm.All().Single().BeatmapMD5Hashes.ToArray(); string updatedHash = updated.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash; diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index fd0b391d0d..b8073a65bc 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -128,7 +128,7 @@ namespace osu.Game.Tests.Database realm.RegisterCustomSubscription(r => { - var subscription = r.All().QueryAsyncWithNotifications((_, _, _) => + var subscription = r.All().QueryAsyncWithNotifications((_, _) => { realm.Run(_ => { diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index d853e75db0..cea30acf3f 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -355,7 +355,7 @@ namespace osu.Game.Tests.Database return null; }); - void gotChange(IRealmCollection sender, ChangeSet changes, Exception error) + void gotChange(IRealmCollection sender, ChangeSet? changes) { changesTriggered++; } diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs index 4ee302bbd0..45842a952a 100644 --- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs +++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs @@ -1,7 +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.Collections.Generic; using System.Linq; using System.Threading; @@ -54,7 +53,7 @@ namespace osu.Game.Tests.Database registration.Dispose(); }); - void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error) + void onChanged(IRealmCollection sender, ChangeSet? changes) { lastChanges = changes; @@ -92,7 +91,7 @@ namespace osu.Game.Tests.Database registration.Dispose(); }); - void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error) => lastChanges = changes; + void onChanged(IRealmCollection sender, ChangeSet? changes) => lastChanges = changes; } [Test] @@ -185,7 +184,7 @@ namespace osu.Game.Tests.Database } }); - void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error) + void onChanged(IRealmCollection sender, ChangeSet? changes) { if (changes == null) resolvedItems = sender; diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index a5662fa121..8b4c6e2411 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -76,12 +76,12 @@ namespace osu.Game.Tests.Database Available = true, })); - Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.True); + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True); // Availability is updated on construction of a RealmRulesetStore var _ = new RealmRulesetStore(realm, storage); - Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.False); + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.False); }); } @@ -101,18 +101,18 @@ namespace osu.Game.Tests.Database Available = true, })); - Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.True); + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True); // Availability is updated on construction of a RealmRulesetStore var _ = new RealmRulesetStore(realm, storage); - Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.False); + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.False); // Simulate the ruleset getting updated LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION; var __ = new RealmRulesetStore(realm, storage); - Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.True); + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True); }); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs index 80c4e4bce9..b0b9d48cbe 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs @@ -131,7 +131,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("results screen score has matching", () => (Player.GetChildScreen() as ResultsScreen)?.Score.Mods.First(), () => Is.EqualTo(playerMods.First())); AddUntilStep("score in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null)); - AddUntilStep("databased score has correct mods", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID)).Mods.First(), () => Is.EqualTo(playerMods.First())); + AddUntilStep("databased score has correct mods", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID))!.Mods.First(), () => Is.EqualTo(playerMods.First())); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index 2d54a4e566..bf6d8e524f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.UserInterface var testPresets = createTestPresets(); foreach (var preset in testPresets) - preset.Ruleset = realm.Find(preset.Ruleset.ShortName); + preset.Ruleset = realm.Find(preset.Ruleset.ShortName)!; realm.Add(testPresets); }); @@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.UserInterface new ManiaModNightcore(), new ManiaModHardRock() }, - Ruleset = r.Find("mania") + Ruleset = r.Find("mania")! }))); AddUntilStep("2 panels visible", () => this.ChildrenOfType().Count() == 2); @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.UserInterface new OsuModHidden(), new OsuModHardRock() }, - Ruleset = r.Find("osu") + Ruleset = r.Find("osu")! }))); AddUntilStep("2 panels visible", () => this.ChildrenOfType().Count() == 2); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index dcb1f730a2..4cb6899ebc 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.UserInterface { Name = "AR0", Description = "Too... many... circles...", - Ruleset = r.Find(OsuRuleset.SHORT_NAME), + Ruleset = r.Find(OsuRuleset.SHORT_NAME)!, Mods = new[] { new OsuModDifficultyAdjust @@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.UserInterface { Name = "Half Time 0.5x", Description = "Very slow", - Ruleset = r.Find(OsuRuleset.SHORT_NAME), + Ruleset = r.Find(OsuRuleset.SHORT_NAME)!, Mods = new[] { new OsuModHalfTime diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 9fe3a41b03..34c1786682 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -102,7 +102,7 @@ namespace osu.Game } } - r.Find(ruleset.ShortName).LastAppliedDifficultyVersion = currentVersion; + r.Find(ruleset.ShortName)!.LastAppliedDifficultyVersion = currentVersion; }); Logger.Log($"Finished resetting {countReset} beatmap sets for {ruleset.Name}"); @@ -184,7 +184,7 @@ namespace osu.Game // ReSharper disable once MethodHasAsyncOverload realmAccess.Write(r => { - r.Find(id).MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics); + r.Find(id)!.MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics); }); Logger.Log($"Populated maximum statistics for score {id}"); @@ -237,7 +237,7 @@ namespace osu.Game // ReSharper disable once MethodHasAsyncOverload realmAccess.Write(r => { - ScoreInfo s = r.Find(id); + ScoreInfo s = r.Find(id)!; s.TotalScore = newTotalScore; s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION; }); diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index fd766490fc..dba3987524 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -69,7 +69,7 @@ namespace osu.Game.Beatmaps Logger.Log($"Beatmap \"{updated}\" update completed successfully", LoggingTarget.Database); - original = realm.Find(original.ID); + original = realm!.Find(original.ID)!; // Generally the import process will do this for us if the OnlineIDs match, // but that isn't a guarantee (ie. if the .osu file doesn't have OnlineIDs populated). diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 73811b2e62..cf5ed25404 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -208,7 +208,7 @@ namespace osu.Game.Beatmaps using (var transaction = r.BeginWrite()) { if (!beatmapInfo.IsManaged) - beatmapInfo = r.Find(beatmapInfo.ID); + beatmapInfo = r.Find(beatmapInfo.ID)!; beatmapInfo.Hidden = true; transaction.Commit(); @@ -227,7 +227,7 @@ namespace osu.Game.Beatmaps using (var transaction = r.BeginWrite()) { if (!beatmapInfo.IsManaged) - beatmapInfo = r.Find(beatmapInfo.ID); + beatmapInfo = r.Find(beatmapInfo.ID)!; beatmapInfo.Hidden = false; transaction.Commit(); @@ -330,7 +330,7 @@ namespace osu.Game.Beatmaps Realm.Write(r => { if (!beatmapInfo.IsManaged) - beatmapInfo = r.Find(beatmapInfo.ID); + beatmapInfo = r.Find(beatmapInfo.ID)!; Debug.Assert(beatmapInfo.BeatmapSet != null); Debug.Assert(beatmapInfo.File != null); @@ -460,7 +460,7 @@ namespace osu.Game.Beatmaps Realm.Write(r => { - var liveBeatmapSet = r.Find(setInfo.ID); + var liveBeatmapSet = r.Find(setInfo.ID)!; setInfo.CopyChangesToRealm(liveBeatmapSet); diff --git a/osu.Game/Beatmaps/BeatmapUpdater.cs b/osu.Game/Beatmaps/BeatmapUpdater.cs index 046adb8327..56bfdc5001 100644 --- a/osu.Game/Beatmaps/BeatmapUpdater.cs +++ b/osu.Game/Beatmaps/BeatmapUpdater.cs @@ -52,7 +52,7 @@ namespace osu.Game.Beatmaps /// /// The managed beatmap set to update. A transaction will be opened to apply changes. /// The preferred scope to use for metadata lookup. - public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapSet.Realm.Write(r => + public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapSet.Realm!.Write(_ => { // Before we use below, we want to invalidate. workingBeatmapCache.Invalidate(beatmapSet); diff --git a/osu.Game/Collections/CollectionDropdown.cs b/osu.Game/Collections/CollectionDropdown.cs index e95565a5c8..e435992381 100644 --- a/osu.Game/Collections/CollectionDropdown.cs +++ b/osu.Game/Collections/CollectionDropdown.cs @@ -59,7 +59,7 @@ namespace osu.Game.Collections Current.BindValueChanged(selectionChanged); } - private void collectionsChanged(IRealmCollection collections, ChangeSet? changes, Exception error) + private void collectionsChanged(IRealmCollection collections, ChangeSet? changes) { var selectedItem = SelectedItem?.Value?.Collection; diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index 0fdf196c4a..6fe38a3229 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -41,7 +41,7 @@ namespace osu.Game.Collections realmSubscription = realm.RegisterForNotifications(r => r.All().OrderBy(c => c.Name), collectionsChanged); } - private void collectionsChanged(IRealmCollection collections, ChangeSet? changes, Exception error) + private void collectionsChanged(IRealmCollection collections, ChangeSet? changes) { Items.Clear(); Items.AddRange(collections.AsEnumerable().Select(c => c.ToLive(realm))); diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 0ab0ff520d..4131148f3f 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -197,7 +197,7 @@ namespace osu.Game.Collections return true; } - private void deleteCollection() => collection.PerformWrite(c => c.Realm.Remove(c)); + private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c)); } } } diff --git a/osu.Game/Database/EmptyRealmSet.cs b/osu.Game/Database/EmptyRealmSet.cs index 7db946d79f..02dfa50fe5 100644 --- a/osu.Game/Database/EmptyRealmSet.cs +++ b/osu.Game/Database/EmptyRealmSet.cs @@ -19,8 +19,8 @@ namespace osu.Game.Database IEnumerator IEnumerable.GetEnumerator() => emptySet.GetEnumerator(); public int Count => emptySet.Count; public T this[int index] => emptySet[index]; - public int IndexOf(object item) => emptySet.IndexOf((T)item); - public bool Contains(object item) => emptySet.Contains((T)item); + public int IndexOf(object? item) => item == null ? -1 : emptySet.IndexOf((T)item); + public bool Contains(object? item) => item != null && emptySet.Contains((T)item); public event NotifyCollectionChangedEventHandler? CollectionChanged { diff --git a/osu.Game/Database/ModelManager.cs b/osu.Game/Database/ModelManager.cs index 7d1dc5239a..47feb8a8f9 100644 --- a/osu.Game/Database/ModelManager.cs +++ b/osu.Game/Database/ModelManager.cs @@ -34,13 +34,13 @@ namespace osu.Game.Database } public void DeleteFile(TModel item, RealmNamedFileUsage file) => - performFileOperation(item, managed => DeleteFile(managed, managed.Files.First(f => f.Filename == file.Filename), managed.Realm)); + performFileOperation(item, managed => DeleteFile(managed, managed.Files.First(f => f.Filename == file.Filename), managed.Realm!)); public void ReplaceFile(TModel item, RealmNamedFileUsage file, Stream contents) => - performFileOperation(item, managed => ReplaceFile(file, contents, managed.Realm)); + performFileOperation(item, managed => ReplaceFile(file, contents, managed.Realm!)); public void AddFile(TModel item, Stream contents, string filename) => - performFileOperation(item, managed => AddFile(managed, contents, filename, managed.Realm)); + performFileOperation(item, managed => AddFile(managed, contents, filename, managed.Realm!)); private void performFileOperation(TModel item, Action operation) { @@ -178,13 +178,14 @@ namespace osu.Game.Database // (ie. if an async import finished very recently). return Realm.Write(realm => { - if (!item.IsManaged) - item = realm.Find(item.ID); + TModel? processableItem = item; + if (!processableItem.IsManaged) + processableItem = realm.Find(item.ID); - if (item?.DeletePending != false) + if (processableItem?.DeletePending != false) return false; - item.DeletePending = true; + processableItem.DeletePending = true; return true; }); } @@ -195,13 +196,14 @@ namespace osu.Game.Database // (ie. if an async import finished very recently). Realm.Write(realm => { - if (!item.IsManaged) - item = realm.Find(item.ID); + TModel? processableItem = item; + if (!processableItem.IsManaged) + processableItem = realm.Find(item.ID); - if (item?.DeletePending != true) + if (processableItem?.DeletePending != true) return; - item.DeletePending = false; + processableItem.DeletePending = false; }); } diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 2bc932f307..a6cef82c5f 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -535,7 +535,7 @@ namespace osu.Game.Database lock (notificationsResetMap) { // Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing. - notificationsResetMap.Add(action, () => callback(new EmptyRealmSet(), null, null)); + notificationsResetMap.Add(action, () => callback(new EmptyRealmSet(), null)); } return RegisterCustomSubscription(action); @@ -755,10 +755,10 @@ namespace osu.Game.Database for (int i = 0; i < itemCount; i++) { - dynamic? oldItem = oldItems.ElementAt(i); - dynamic? newItem = newItems.ElementAt(i); + dynamic oldItem = oldItems.ElementAt(i); + dynamic newItem = newItems.ElementAt(i); - long? nullableOnlineID = oldItem?.OnlineID; + long? nullableOnlineID = oldItem.OnlineID; newItem.OnlineID = (int)(nullableOnlineID ?? -1); } } @@ -795,7 +795,7 @@ namespace osu.Game.Database for (int i = 0; i < metadataCount; i++) { - dynamic? oldItem = oldMetadata.ElementAt(i); + dynamic oldItem = oldMetadata.ElementAt(i); var newItem = newMetadata.ElementAt(i); string username = oldItem.Author; @@ -818,7 +818,7 @@ namespace osu.Game.Database for (int i = 0; i < newSettings.Count; i++) { - dynamic? oldItem = oldSettings.ElementAt(i); + dynamic oldItem = oldSettings.ElementAt(i); var newItem = newSettings.ElementAt(i); long rulesetId = oldItem.RulesetID; @@ -843,7 +843,7 @@ namespace osu.Game.Database for (int i = 0; i < newKeyBindings.Count; i++) { - dynamic? oldItem = oldKeyBindings.ElementAt(i); + dynamic oldItem = oldKeyBindings.ElementAt(i); var newItem = newKeyBindings.ElementAt(i); if (oldItem.RulesetID == null) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 9c871a3929..d96ccd9dcd 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -104,7 +104,7 @@ namespace osu.Game.Database PerformRead(t => { - using (var transaction = t.Realm.BeginWrite()) + using (var transaction = t.Realm!.BeginWrite()) { perform(t); transaction.Commit(); @@ -133,7 +133,7 @@ namespace osu.Game.Database { Debug.Assert(ThreadSafety.IsUpdateThread); - if (dataIsFromUpdateThread && !data.Realm.IsClosed) + if (dataIsFromUpdateThread && !data.Realm!.IsClosed) { RealmLiveStatistics.USAGE_UPDATE_IMMEDIATE.Value++; return; @@ -154,7 +154,7 @@ namespace osu.Game.Database // To ensure that behaviour matches what we'd expect (the object *is* available), force // a refresh to bring in any off-thread changes immediately. realm.Refresh(); - found = realm.Find(ID); + found = realm.Find(ID)!; } return found; diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 5a6c2e3232..77d199cf9c 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -43,7 +43,7 @@ namespace osu.Game.Database .ForMember(s => s.BeatmapSet, cc => cc.Ignore()) .AfterMap((s, d) => { - d.Ruleset = d.Realm.Find(s.Ruleset.ShortName); + d.Ruleset = d.Realm!.Find(s.Ruleset.ShortName)!; copyChangesToRealm(s.Difficulty, d.Difficulty); copyChangesToRealm(s.Metadata, d.Metadata); }); @@ -57,7 +57,7 @@ namespace osu.Game.Database // Importantly, search all of realm for the beatmap (not just the set's beatmaps). // It may have gotten detached, and if that's the case let's use this opportunity to fix // things up. - var existingBeatmap = d.Realm.Find(beatmap.ID); + var existingBeatmap = d.Realm!.Find(beatmap.ID); if (existingBeatmap != null) { @@ -77,7 +77,7 @@ namespace osu.Game.Database { ID = beatmap.ID, BeatmapSet = d, - Ruleset = d.Realm.Find(beatmap.Ruleset.ShortName) + Ruleset = d.Realm.Find(beatmap.Ruleset.ShortName)! }; d.Beatmaps.Add(newBeatmap); @@ -287,7 +287,7 @@ namespace osu.Game.Database /// /// /// - public static IDisposable? QueryAsyncWithNotifications(this IRealmCollection collection, NotificationCallbackDelegate callback) + public static IDisposable QueryAsyncWithNotifications(this IRealmCollection collection, NotificationCallbackDelegate callback) where T : RealmObjectBase { if (!RealmAccess.CurrentThreadSubscriptionsAllowed) diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index fab0be6cf0..be025e3aa2 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -51,7 +51,7 @@ namespace osu.Game.Input.Bindings protected override void LoadComplete() { - realmSubscription = realm.RegisterForNotifications(queryRealmKeyBindings, (sender, _, _) => + realmSubscription = realm.RegisterForNotifications(queryRealmKeyBindings, (sender, _) => { // The first fire of this is a bit redundant as this is being called in base.LoadComplete, // but this is safest in case the subscription is restored after a context recycle. diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index 144c4445a3..3db602c353 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -40,7 +40,7 @@ namespace osu.Game.Online // Used to interact with manager classes that don't support interface types. Will eventually be replaced. var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID }; - realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, _, _) => + realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, _) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 331a471ad5..ceb8e53778 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -107,7 +107,7 @@ namespace osu.Game.Online.Rooms // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = realm.RegisterForNotifications(_ => filteredBeatmaps(), (_, changes, _) => + realmSubscription = realm.RegisterForNotifications(_ => filteredBeatmaps(), (_, changes) => { if (changes == null) return; diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index 4ddcb40368..de42292372 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -48,7 +48,7 @@ namespace osu.Game.Online realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || (!string.IsNullOrEmpty(s.Hash) && s.Hash == TrackedItem.Hash)) - && !s.DeletePending), (items, _, _) => + && !s.DeletePending), (items, _) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs index 75bc8fd3a8..385695f669 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs @@ -123,7 +123,7 @@ namespace osu.Game.Overlays.FirstRunSetup beatmapSubscription?.Dispose(); } - private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes, Exception error) => Schedule(() => + private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) => Schedule(() => { currentlyLoadedBeatmaps.Text = FirstRunSetupBeatmapScreenStrings.CurrentlyLoadedBeatmaps(sender.Count); diff --git a/osu.Game/Overlays/Mods/AddPresetPopover.cs b/osu.Game/Overlays/Mods/AddPresetPopover.cs index d9e350e560..ef855f6166 100644 --- a/osu.Game/Overlays/Mods/AddPresetPopover.cs +++ b/osu.Game/Overlays/Mods/AddPresetPopover.cs @@ -102,7 +102,7 @@ namespace osu.Game.Overlays.Mods Name = nameTextBox.Current.Value, Description = descriptionTextBox.Current.Value, Mods = selectedMods.Value.ToArray(), - Ruleset = r.Find(ruleset.Value.ShortName) + Ruleset = r.Find(ruleset.Value.ShortName)! })); this.HidePopover(); diff --git a/osu.Game/Overlays/Mods/ModPresetColumn.cs b/osu.Game/Overlays/Mods/ModPresetColumn.cs index bf5e576277..3b12eec195 100644 --- a/osu.Game/Overlays/Mods/ModPresetColumn.cs +++ b/osu.Game/Overlays/Mods/ModPresetColumn.cs @@ -61,7 +61,7 @@ namespace osu.Game.Overlays.Mods private Task? latestLoadTask; internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true; - private void asyncLoadPanels(IRealmCollection presets, ChangeSet changes, Exception error) + private void asyncLoadPanels(IRealmCollection presets, ChangeSet? changes) { cancellationTokenSource?.Cancel(); diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 43b9024303..7784643163 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -109,7 +109,7 @@ namespace osu.Game.Overlays.Music beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo.ToLive(realm), true); } - private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) + private void beatmapsChanged(IRealmCollection sender, ChangeSet changes) { if (changes == null) { diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 5382eac675..e997e70157 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Settings.Sections }); } - private void skinsChanged(IRealmCollection sender, ChangeSet changes, Exception error) + private void skinsChanged(IRealmCollection sender, ChangeSet changes) { // This can only mean that realm is recycling, else we would see the protected skins. // Because we are using `Live<>` in this class, we don't need to worry about this scenario too much. diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 5ada2a410d..264a23b6d3 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -66,10 +66,10 @@ namespace osu.Game.Scoring { // Ensure the beatmap is not detached. if (!model.BeatmapInfo.IsManaged) - model.BeatmapInfo = realm.Find(model.BeatmapInfo.ID); + model.BeatmapInfo = realm.Find(model.BeatmapInfo.ID)!; if (!model.Ruleset.IsManaged) - model.Ruleset = realm.Find(model.Ruleset.ShortName); + model.Ruleset = realm.Find(model.Ruleset.ShortName)!; // These properties are known to be non-null, but these final checks ensure a null hasn't come from somewhere (or the refetch has failed). // Under no circumstance do we want these to be written to realm as null. diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 3d87a57295..9af9a0ce72 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -223,7 +223,7 @@ namespace osu.Game.Screens.Select subscriptionHiddenBeatmaps = realm.RegisterForNotifications(r => r.All().Where(b => b.Hidden), beatmapsChanged); } - private void deletedBeatmapSetsChanged(IRealmCollection sender, ChangeSet? changes, Exception? error) + private void deletedBeatmapSetsChanged(IRealmCollection sender, ChangeSet? changes) { // If loading test beatmaps, avoid overwriting with realm subscription callbacks. if (loadedTestBeatmaps) @@ -236,7 +236,7 @@ namespace osu.Game.Screens.Select removeBeatmapSet(sender[i].ID); } - private void beatmapSetsChanged(IRealmCollection sender, ChangeSet? changes, Exception? error) + private void beatmapSetsChanged(IRealmCollection sender, ChangeSet? changes) { // If loading test beatmaps, avoid overwriting with realm subscription callbacks. if (loadedTestBeatmaps) @@ -255,7 +255,7 @@ namespace osu.Game.Screens.Select foreach (var id in realmSets) { if (!root.BeatmapSetsByID.ContainsKey(id)) - UpdateBeatmapSet(realm.Realm.Find(id).Detach()); + UpdateBeatmapSet(realm.Realm.Find(id)!.Detach()); } foreach (var id in root.BeatmapSetsByID.Keys) @@ -315,7 +315,7 @@ namespace osu.Game.Screens.Select } } - private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes, Exception? error) + private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) { // we only care about actual changes in hidden status. if (changes == null) diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index 7c632b63db..c17de77619 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Select.Carousel localScoresChanged); }, true); - void localScoresChanged(IRealmCollection sender, ChangeSet? changes, Exception _) + void localScoresChanged(IRealmCollection sender, ChangeSet? changes) { // This subscription may fire from changes to linked beatmaps, which we don't care about. // It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications. diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 4c41ed3622..58c14b15b9 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -193,7 +193,7 @@ namespace osu.Game.Screens.Select.Leaderboards + $" AND {nameof(ScoreInfo.DeletePending)} == false" , beatmapInfo.ID, ruleset.Value.ShortName), localScoresChanged); - void localScoresChanged(IRealmCollection sender, ChangeSet? changes, Exception exception) + void localScoresChanged(IRealmCollection sender, ChangeSet? changes) { if (cancellationToken.IsCancellationRequested) return; diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 2b56767bd0..48b5c210b8 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -90,7 +90,7 @@ namespace osu.Game.Screens.Spectate })); } - private void beatmapsChanged(IRealmCollection items, ChangeSet changes, Exception ___) + private void beatmapsChanged(IRealmCollection items, ChangeSet changes) { if (changes?.InsertedIndices == null) return; diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index cc887a7a61..cce099a268 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -38,7 +38,7 @@ namespace osu.Game.Skinning realmSubscription?.Dispose(); } - private void skinChanged(IRealmCollection sender, ChangeSet changes, Exception error) => invalidateCache(); + private void skinChanged(IRealmCollection sender, ChangeSet? changes) => invalidateCache(); protected override IEnumerable GetFilenames(string name) { diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index 43760c4a19..f2103a45c4 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -198,7 +198,7 @@ namespace osu.Game.Skinning using (var streamContent = new MemoryStream(Encoding.UTF8.GetBytes(skinInfoJson))) { - modelManager.AddFile(s, streamContent, skin_info_file, s.Realm); + modelManager.AddFile(s, streamContent, skin_info_file, s.Realm!); } // Then serialise each of the drawable component groups into respective files. @@ -213,9 +213,9 @@ namespace osu.Game.Skinning var oldFile = s.GetFile(filename); if (oldFile != null) - modelManager.ReplaceFile(oldFile, streamContent, s.Realm); + modelManager.ReplaceFile(oldFile, streamContent, s.Realm!); else - modelManager.AddFile(s, streamContent, filename, s.Realm); + modelManager.AddFile(s, streamContent, filename, s.Realm!); } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index b4d8dd513f..41712f428a 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 5af4aa874158157ce99aa7c9a1383c4b020409c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 14:05:47 +0900 Subject: [PATCH 0553/2100] Avoid strong hits cutting off other strong hits --- osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 2797492b0a..7144fd8ca5 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -79,11 +79,19 @@ namespace osu.Game.Rulesets.Taiko.UI return false; } - if (strong && hitType == HitType.Centre) - flushCenterTriggerSources(); + if (strong) + { + switch (hitType) + { + case HitType.Centre: + flushCenterTriggerSources(); + break; - if (strong && hitType == HitType.Rim) - flushRimTriggerSources(); + case HitType.Rim: + flushRimTriggerSources(); + break; + } + } triggerSource.Play(hitType, strong); @@ -124,14 +132,12 @@ namespace osu.Game.Rulesets.Taiko.UI { leftCentreTrigger.StopAllPlayback(); rightCentreTrigger.StopAllPlayback(); - strongCentreTrigger.StopAllPlayback(); } private void flushRimTriggerSources() { leftRimTrigger.StopAllPlayback(); rightRimTrigger.StopAllPlayback(); - strongRimTrigger.StopAllPlayback(); } public void OnReleased(KeyBindingReleaseEvent e) From af3f9086e51a5f54f914a813f70685640236c4c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 14:02:20 +0900 Subject: [PATCH 0554/2100] Expose rewinding state of `IGameplayClock`s The implementation of this requires a bit of a special case for 0, so makes sense to implement in a central place. --- .../Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs | 1 + osu.Game/Beatmaps/FramedBeatmapClock.cs | 5 +++++ osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 5 +++++ osu.Game/Screens/Play/GameplayClockContainer.cs | 5 ++--- osu.Game/Screens/Play/IGameplayClock.cs | 8 ++++++++ 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs index bcb5291108..db06329d74 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs @@ -125,6 +125,7 @@ namespace osu.Game.Tests.Visual.Gameplay public IEnumerable NonGameplayAdjustments => throw new NotImplementedException(); public IBindable IsPaused => throw new NotImplementedException(); + public bool IsRewinding => false; } } } diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 080b0ce7ec..399e2fde7f 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -64,6 +64,8 @@ namespace osu.Game.Beatmaps [Resolved] private IBindable beatmap { get; set; } = null!; + public bool IsRewinding { get; private set; } + public bool IsCoupled { get => decoupledClock.IsCoupled; @@ -133,6 +135,9 @@ namespace osu.Game.Beatmaps } else finalClockSource.ProcessFrame(); + + if (Clock.ElapsedFrameTime != 0) + IsRewinding = Clock.ElapsedFrameTime < 0; } public double TotalAppliedOffset diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 4bb145973d..90cffab714 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -171,6 +171,9 @@ namespace osu.Game.Rulesets.UI // The manual clock time has changed in the above code. The framed clock now needs to be updated // to ensure that the its time is valid for our children before input is processed framedClock.ProcessFrame(); + + if (framedClock.ElapsedFrameTime != 0) + IsRewinding = framedClock.ElapsedFrameTime < 0; } /// @@ -247,6 +250,8 @@ namespace osu.Game.Rulesets.UI public IBindable IsPaused { get; } = new BindableBool(); + public bool IsRewinding { get; private set; } + public double CurrentTime => framedClock.CurrentTime; public double Rate => framedClock.Rate; diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index c42f607908..22e6884526 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -19,11 +19,10 @@ namespace osu.Game.Screens.Play [Cached(typeof(IGameplayClock))] public partial class GameplayClockContainer : Container, IAdjustableClock, IGameplayClock { - /// - /// Whether gameplay is paused. - /// public IBindable IsPaused => isPaused; + public bool IsRewinding => GameplayClock.IsRewinding; + /// /// The source clock. Should generally not be used for any timekeeping purposes. /// diff --git a/osu.Game/Screens/Play/IGameplayClock.cs b/osu.Game/Screens/Play/IGameplayClock.cs index 83ba5f3474..ad28e343ff 100644 --- a/osu.Game/Screens/Play/IGameplayClock.cs +++ b/osu.Game/Screens/Play/IGameplayClock.cs @@ -23,6 +23,14 @@ namespace osu.Game.Screens.Play /// IAdjustableAudioComponent AdjustmentsFromMods { get; } + /// + /// Whether gameplay is paused. + /// IBindable IsPaused { get; } + + /// + /// Whether the clock is currently rewinding. + /// + bool IsRewinding { get; } } } From de74c9eb8bb8c24c7a0959969fcb0d9986dacb97 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 14:06:40 +0900 Subject: [PATCH 0555/2100] Fix `GameplaySampleTriggerSource` not handling rewinds correctly --- osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index 18d412ab44..fed262868d 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -85,6 +85,14 @@ namespace osu.Game.Rulesets.UI sound.Stop(); }); + protected override void Update() + { + base.Update(); + + if (gameplayClock?.IsRewinding == true) + mostValidObject = null; + } + protected HitObject? GetMostValidObject() { if (mostValidObject == null || isAlreadyHit(mostValidObject)) From f69f6adf670f29d8a299ce3ef01b5609a2960ae4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 16:15:42 +0900 Subject: [PATCH 0556/2100] Remove beatmap info wedge rotation animation It looks jank and also causes framebuffer overheads. But mostly because it looks jank. --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 5dd2486e1c..3605e3d706 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -76,14 +76,12 @@ namespace osu.Game.Screens.Select protected override void PopIn() { this.MoveToX(0, animation_duration, Easing.OutQuint); - this.RotateTo(0, animation_duration, Easing.OutQuint); this.FadeIn(transition_duration); } protected override void PopOut() { this.MoveToX(-100, animation_duration, Easing.In); - this.RotateTo(10, animation_duration, Easing.In); this.FadeOut(transition_duration * 2, Easing.In); } From b40532dde167c056bccfde1c26d21ed7e348fdae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 17:17:16 +0900 Subject: [PATCH 0557/2100] Fix tournament bracket parsing regression Closes #24136. Regressed in #24037. --- osu.Game.Tournament/TournamentGameBase.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 7d0571dde0..29f0f4f356 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -9,7 +9,6 @@ using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json; using osu.Framework.Allocation; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Framework.Input; @@ -94,7 +93,7 @@ namespace osu.Game.Tournament Task.Run(readBracket); } - private void readBracket() + private async Task readBracket() { try { @@ -102,7 +101,7 @@ namespace osu.Game.Tournament { using (Stream stream = storage.GetStream(BRACKET_FILENAME, FileAccess.Read, FileMode.Open)) using (var sr = new StreamReader(stream)) - ladder = JsonConvert.DeserializeObject(sr.ReadToEnd(), new JsonPointConverter()); + ladder = JsonConvert.DeserializeObject(await sr.ReadToEndAsync().ConfigureAwait(false), new JsonPointConverter()); } ladder ??= new LadderInfo(); @@ -166,8 +165,8 @@ namespace osu.Game.Tournament } addedInfo |= addPlayers(); - addedInfo |= addRoundBeatmaps(); - addedInfo |= addSeedingBeatmaps(); + addedInfo |= await addRoundBeatmaps().ConfigureAwait(false); + addedInfo |= await addSeedingBeatmaps().ConfigureAwait(false); if (addedInfo) saveChanges(); @@ -233,7 +232,7 @@ namespace osu.Game.Tournament /// /// Add missing beatmap info based on beatmap IDs /// - private bool addRoundBeatmaps() + private async Task addRoundBeatmaps() { var beatmapsRequiringPopulation = ladder.Rounds .SelectMany(r => r.Beatmaps) @@ -246,7 +245,7 @@ namespace osu.Game.Tournament { var b = beatmapsRequiringPopulation[i]; - b.Beatmap = new TournamentBeatmap(beatmapCache.GetBeatmapAsync(b.ID).GetResultSafely() ?? new APIBeatmap()); + b.Beatmap = new TournamentBeatmap(await beatmapCache.GetBeatmapAsync(b.ID).ConfigureAwait(false) ?? new APIBeatmap()); updateLoadProgressMessage($"Populating round beatmaps ({i} / {beatmapsRequiringPopulation.Count})"); } @@ -257,7 +256,7 @@ namespace osu.Game.Tournament /// /// Add missing beatmap info based on beatmap IDs /// - private bool addSeedingBeatmaps() + private async Task addSeedingBeatmaps() { var beatmapsRequiringPopulation = ladder.Teams .SelectMany(r => r.SeedingResults) @@ -271,7 +270,7 @@ namespace osu.Game.Tournament { var b = beatmapsRequiringPopulation[i]; - b.Beatmap = new TournamentBeatmap(beatmapCache.GetBeatmapAsync(b.ID).GetResultSafely() ?? new APIBeatmap()); + b.Beatmap = new TournamentBeatmap(await beatmapCache.GetBeatmapAsync(b.ID).ConfigureAwait(false) ?? new APIBeatmap()); updateLoadProgressMessage($"Populating seeding beatmaps ({i} / {beatmapsRequiringPopulation.Count})"); } From c9fd4354024b03fc1009388eb58853bc3f12de48 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 17:58:27 +0900 Subject: [PATCH 0558/2100] Fix potential crash when mashing exit key --- osu.Game/Overlays/Dialog/PopupDialog.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index 9969677826..e1e5604e4c 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -225,7 +225,12 @@ namespace osu.Game.Overlays.Dialog /// /// Programmatically clicks the first button of the provided type. /// - public void PerformAction() where T : PopupDialogButton => Buttons.OfType().First().TriggerClick(); + public void PerformAction() where T : PopupDialogButton + { + // Buttons are regularly added in BDL or LoadComplete, so let's schedule to ensure + // they are ready to be pressed. + Schedule(() => Buttons.OfType().First().TriggerClick()); + } protected override bool OnKeyDown(KeyDownEvent e) { From 7a3a14e50def1ee576a92c102b377ae8b86c2cd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 17:33:47 +0900 Subject: [PATCH 0559/2100] Add sample trigger tests covering rewinding of gameplay --- .../Gameplay/TestSceneGameplaySampleTriggerSource.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index 6701871e8d..8938cff9a3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -107,7 +107,7 @@ namespace osu.Game.Tests.Visual.Gameplay { base.SetUpSteps(); - AddStep("Add trigger source", () => Player.GameplayClockContainer.Add(sampleTriggerSource = new TestGameplaySampleTriggerSource(Player.DrawableRuleset.Playfield.HitObjectContainer))); + AddStep("Add trigger source", () => Player.DrawableRuleset.FrameStableComponents.Add(sampleTriggerSource = new TestGameplaySampleTriggerSource(Player.DrawableRuleset.Playfield.HitObjectContainer))); } [Test] @@ -153,6 +153,14 @@ namespace osu.Game.Tests.Visual.Gameplay waitForAliveObjectIndex(2); checkValidObjectIndex(2); + // test rewinding + seekBeforeIndex(1); + waitForAliveObjectIndex(1); + checkValidObjectIndex(1); + + seekBeforeIndex(1, 400); + checkValidObjectIndex(0); + seekBeforeIndex(3); waitForAliveObjectIndex(3); checkValidObjectIndex(3); From 2e98ab0a481f24502177011d2b31f29fc4697d75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 14:02:20 +0900 Subject: [PATCH 0560/2100] Expose rewinding state of `IGameplayClock`s The implementation of this requires a bit of a special case for 0, so makes sense to implement in a central place. --- .../Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs | 1 + osu.Game/Beatmaps/FramedBeatmapClock.cs | 5 +++++ osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 5 +++++ osu.Game/Screens/Play/GameplayClockContainer.cs | 5 ++--- osu.Game/Screens/Play/IGameplayClock.cs | 8 ++++++++ 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs index bcb5291108..db06329d74 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs @@ -125,6 +125,7 @@ namespace osu.Game.Tests.Visual.Gameplay public IEnumerable NonGameplayAdjustments => throw new NotImplementedException(); public IBindable IsPaused => throw new NotImplementedException(); + public bool IsRewinding => false; } } } diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 080b0ce7ec..399e2fde7f 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -64,6 +64,8 @@ namespace osu.Game.Beatmaps [Resolved] private IBindable beatmap { get; set; } = null!; + public bool IsRewinding { get; private set; } + public bool IsCoupled { get => decoupledClock.IsCoupled; @@ -133,6 +135,9 @@ namespace osu.Game.Beatmaps } else finalClockSource.ProcessFrame(); + + if (Clock.ElapsedFrameTime != 0) + IsRewinding = Clock.ElapsedFrameTime < 0; } public double TotalAppliedOffset diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 4bb145973d..90cffab714 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -171,6 +171,9 @@ namespace osu.Game.Rulesets.UI // The manual clock time has changed in the above code. The framed clock now needs to be updated // to ensure that the its time is valid for our children before input is processed framedClock.ProcessFrame(); + + if (framedClock.ElapsedFrameTime != 0) + IsRewinding = framedClock.ElapsedFrameTime < 0; } /// @@ -247,6 +250,8 @@ namespace osu.Game.Rulesets.UI public IBindable IsPaused { get; } = new BindableBool(); + public bool IsRewinding { get; private set; } + public double CurrentTime => framedClock.CurrentTime; public double Rate => framedClock.Rate; diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index c42f607908..22e6884526 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -19,11 +19,10 @@ namespace osu.Game.Screens.Play [Cached(typeof(IGameplayClock))] public partial class GameplayClockContainer : Container, IAdjustableClock, IGameplayClock { - /// - /// Whether gameplay is paused. - /// public IBindable IsPaused => isPaused; + public bool IsRewinding => GameplayClock.IsRewinding; + /// /// The source clock. Should generally not be used for any timekeeping purposes. /// diff --git a/osu.Game/Screens/Play/IGameplayClock.cs b/osu.Game/Screens/Play/IGameplayClock.cs index 83ba5f3474..ad28e343ff 100644 --- a/osu.Game/Screens/Play/IGameplayClock.cs +++ b/osu.Game/Screens/Play/IGameplayClock.cs @@ -23,6 +23,14 @@ namespace osu.Game.Screens.Play /// IAdjustableAudioComponent AdjustmentsFromMods { get; } + /// + /// Whether gameplay is paused. + /// IBindable IsPaused { get; } + + /// + /// Whether the clock is currently rewinding. + /// + bool IsRewinding { get; } } } From 753db044b44bfcbec073637ae4273ba7bc84dd68 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 14:06:40 +0900 Subject: [PATCH 0561/2100] Fix `GameplaySampleTriggerSource` not handling rewinds correctly --- osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index c554318357..d6375ad282 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -69,6 +69,14 @@ namespace osu.Game.Rulesets.UI hitSound.Play(); }); + protected override void Update() + { + base.Update(); + + if (gameplayClock?.IsRewinding == true) + mostValidObject = null; + } + protected HitObject? GetMostValidObject() { if (mostValidObject == null || isAlreadyHit(mostValidObject)) From 070b3883ce534cf6d9c5a40f8a329ac47e2bff16 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 00:31:32 +0900 Subject: [PATCH 0562/2100] Remove the ability to cancel all "in progress" tasks --- osu.Game/Localisation/NotificationsStrings.cs | 5 --- osu.Game/Overlays/NotificationOverlay.cs | 4 +-- .../Notifications/NotificationSection.cs | 32 +++++++++---------- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 44e440e8d9..53687f2b28 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -29,11 +29,6 @@ namespace osu.Game.Localisation /// public static LocalisableString ClearAll => new TranslatableString(getKey(@"clear_all"), @"Clear All"); - /// - /// "Cancel All" - /// - public static LocalisableString CancelAll => new TranslatableString(getKey(@"cancel_all"), @"Cancel All"); - /// /// "Your battery level is low! Charge your device to prevent interruptions during gameplay." /// diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 21027b0931..5c0427bf03 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -108,8 +108,8 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.X, Children = new[] { - new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }, NotificationsStrings.ClearAll), - new NotificationSection(NotificationsStrings.RunningTasks, new[] { typeof(ProgressNotification) }, NotificationsStrings.CancelAll), + new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }, true), + new NotificationSection(NotificationsStrings.RunningTasks, new[] { typeof(ProgressNotification) }, false), } } } diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index 4e28ade802..168c6afe27 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -13,6 +13,7 @@ using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; using osuTK; namespace osu.Game.Overlays.Notifications @@ -38,16 +39,16 @@ namespace osu.Game.Overlays.Notifications public IEnumerable AcceptedNotificationTypes { get; } - private readonly LocalisableString clearButtonText; - private readonly LocalisableString titleText; - public NotificationSection(LocalisableString title, IEnumerable acceptedNotificationTypes, LocalisableString clearButtonText) + private readonly bool allowClear; + + public NotificationSection(LocalisableString title, IEnumerable acceptedNotificationTypes, bool allowClear) { AcceptedNotificationTypes = acceptedNotificationTypes.ToArray(); - this.clearButtonText = clearButtonText.ToUpper(); titleText = title; + this.allowClear = allowClear; } [BackgroundDependencyLoader] @@ -71,15 +72,17 @@ namespace osu.Game.Overlays.Notifications { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Children = new Drawable[] + Children = new[] { - new ClearAllButton - { - Text = clearButtonText, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Action = clearAll - }, + allowClear + ? new ClearAllButton + { + Text = NotificationsStrings.ClearAll, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Action = clearAll + } + : Empty(), new FillFlowContainer { Margin = new MarginPadding @@ -115,10 +118,7 @@ namespace osu.Game.Overlays.Notifications }); } - private void clearAll() - { - notifications.Children.ForEach(c => c.Close(true)); - } + private void clearAll() => notifications.Children.ForEach(c => c.Close(true)); protected override void Update() { From fdb572fdea7dd42d5385a2367ef334a79c0a36e8 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 5 Jul 2023 17:25:16 +0900 Subject: [PATCH 0563/2100] Add more/different notification sounds --- osu.Game/Online/Chat/MessageNotifier.cs | 2 ++ osu.Game/Overlays/Notifications/Notification.cs | 2 +- .../Overlays/Notifications/ProgressCompletionNotification.cs | 2 ++ osu.Game/Overlays/Notifications/SimpleErrorNotification.cs | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index ae249d1b7f..65aac723da 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -171,6 +171,8 @@ namespace osu.Game.Online.Chat public abstract partial class HighlightMessageNotification : SimpleNotification { + public override string PopInSampleName => "UI/notification-mention"; + protected HighlightMessageNotification(Message message, Channel channel) { this.message = message; diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 77d3317b1f..e2c6de0635 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -50,7 +50,7 @@ namespace osu.Game.Overlays.Notifications /// public virtual bool DisplayOnTop => true; - public virtual string PopInSampleName => "UI/notification-pop-in"; + public virtual string PopInSampleName => "UI/notification-default"; protected NotificationLight Light; diff --git a/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs b/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs index 46972d4b5e..93286d9d36 100644 --- a/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs @@ -10,6 +10,8 @@ namespace osu.Game.Overlays.Notifications { public partial class ProgressCompletionNotification : SimpleNotification { + public override string PopInSampleName => "UI/notification-done"; + public ProgressCompletionNotification() { Icon = FontAwesome.Solid.Check; diff --git a/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs b/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs index 758eea93d4..81e3b40ffc 100644 --- a/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs +++ b/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs @@ -7,7 +7,7 @@ namespace osu.Game.Overlays.Notifications { public partial class SimpleErrorNotification : SimpleNotification { - public override string PopInSampleName => "UI/error-notification-pop-in"; + public override string PopInSampleName => "UI/notification-error"; public SimpleErrorNotification() { From 4ff4c3a12e271219d8f02d4cec539316bdc21480 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 5 Jul 2023 17:26:20 +0900 Subject: [PATCH 0564/2100] Remove sound from notification closing/hiding --- osu.Game/Overlays/NotificationOverlay.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 21027b0931..ceb5bdd1e9 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -229,14 +229,7 @@ namespace osu.Game.Overlays mainContent.FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.In); } - private void notificationClosed() => Schedule(() => - { - updateCounts(); - - // this debounce is currently shared between popin/popout sounds, which means one could potentially not play when the user is expecting it. - // popout is constant across all notification types, and should therefore be handled using playback concurrency instead, but seems broken at the moment. - playDebouncedSample("UI/overlay-pop-out"); - }); + private void notificationClosed() => Schedule(updateCounts); private void playDebouncedSample(string sampleName) { From 6e2b7f433b6e26d877c5d25a3db2987c7b88d8b4 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 5 Jul 2023 17:27:42 +0900 Subject: [PATCH 0565/2100] Add a sound for 'cancelling' `ProgessNotification` --- osu.Game/Overlays/Notifications/ProgressNotification.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index e6662e2179..f3eba37517 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -4,6 +4,8 @@ using System; using System.Threading; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -122,6 +124,7 @@ namespace osu.Game.Overlays.Notifications cancellationTokenSource.Cancel(); IconContent.FadeColour(ColourInfo.GradientVertical(Color4.Gray, Color4.Gray.Lighten(0.5f)), colour_fade_duration); + cancelSample?.Play(); loadingSpinner.Hide(); var icon = new SpriteIcon @@ -190,6 +193,8 @@ namespace osu.Game.Overlays.Notifications private LoadingSpinner loadingSpinner = null!; + private Sample? cancelSample; + private readonly TextFlowContainer textDrawable; public ProgressNotification() @@ -217,7 +222,7 @@ namespace osu.Game.Overlays.Notifications } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, AudioManager audioManager) { colourQueued = colours.YellowDark; colourActive = colours.Blue; @@ -236,6 +241,8 @@ namespace osu.Game.Overlays.Notifications Size = new Vector2(loading_spinner_size), } }); + + cancelSample = audioManager.Samples.Get(@"UI/notification-cancel"); } public override void Close(bool runFlingAnimation) From d4f5d0c8787088c31753fc054a5e96249c0736ca Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 7 Jul 2023 01:06:09 +0900 Subject: [PATCH 0566/2100] Revert "Remove sound from notification closing/hiding" This reverts commit 244f3c6098bb27b66f5ff7fb8c76f38f56cfb4cd. --- osu.Game/Overlays/NotificationOverlay.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index ceb5bdd1e9..21027b0931 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -229,7 +229,14 @@ namespace osu.Game.Overlays mainContent.FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.In); } - private void notificationClosed() => Schedule(updateCounts); + private void notificationClosed() => Schedule(() => + { + updateCounts(); + + // this debounce is currently shared between popin/popout sounds, which means one could potentially not play when the user is expecting it. + // popout is constant across all notification types, and should therefore be handled using playback concurrency instead, but seems broken at the moment. + playDebouncedSample("UI/overlay-pop-out"); + }); private void playDebouncedSample(string sampleName) { From a55ba963a92b39bdbb80a7c79403e2b59cde1462 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 7 Jul 2023 01:50:15 +0900 Subject: [PATCH 0567/2100] Don't play 'popout' sample when `ProgressNotification` completes --- osu.Game/Overlays/NotificationOverlay.cs | 9 ++++++--- osu.Game/Overlays/Notifications/Notification.cs | 1 + osu.Game/Overlays/Notifications/ProgressNotification.cs | 2 ++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 21027b0931..d21ad625c1 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -169,7 +169,7 @@ namespace osu.Game.Overlays Logger.Log($"⚠️ {notification.Text}"); - notification.Closed += notificationClosed; + notification.Closed += () => notificationClosed(notification); if (notification is IHasCompletionTarget hasCompletionTarget) hasCompletionTarget.CompletionTarget = Post; @@ -229,17 +229,20 @@ namespace osu.Game.Overlays mainContent.FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.In); } - private void notificationClosed() => Schedule(() => + private void notificationClosed(Notification notification) => Schedule(() => { updateCounts(); // this debounce is currently shared between popin/popout sounds, which means one could potentially not play when the user is expecting it. // popout is constant across all notification types, and should therefore be handled using playback concurrency instead, but seems broken at the moment. - playDebouncedSample("UI/overlay-pop-out"); + playDebouncedSample(notification.PopOutSampleName); }); private void playDebouncedSample(string sampleName) { + if (string.IsNullOrEmpty(sampleName)) + return; + if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) { audio.Samples.Get(sampleName)?.Play(); diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index e2c6de0635..8cdc373417 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -51,6 +51,7 @@ namespace osu.Game.Overlays.Notifications public virtual bool DisplayOnTop => true; public virtual string PopInSampleName => "UI/notification-default"; + public virtual string PopOutSampleName => "UI/overlay-pop-out"; protected NotificationLight Light; diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index f3eba37517..53ac490297 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -29,6 +29,8 @@ namespace osu.Game.Overlays.Notifications protected override bool AllowFlingDismiss => false; + public override string PopOutSampleName => State is ProgressNotificationState.Cancelled ? base.PopOutSampleName : ""; + /// /// The function to post completion notifications back to. /// From f3f4bb635680b28c6f47090d2cbc2660992362f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Jul 2023 20:56:24 +0200 Subject: [PATCH 0568/2100] Add one more safety against processing scores with missing beatmap --- osu.Game/BackgroundBeatmapProcessor.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 9c140bdda9..323e783d8d 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -208,7 +208,9 @@ namespace osu.Game { Logger.Log("Querying for scores that need total score conversion..."); - HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All().Where(s => s.TotalScoreVersion == 30000002).AsEnumerable().Select(s => s.ID))); + HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All() + .Where(s => s.BeatmapInfo != null && s.TotalScoreVersion == 30000002) + .AsEnumerable().Select(s => s.ID))); Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); From 1473abd909691d950171bd18be9dd1d639504f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Jul 2023 22:01:02 +0200 Subject: [PATCH 0569/2100] Remove outdated xmldoc --- osu.Game/Database/RealmObjectExtensions.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 77d199cf9c..72529ed9ff 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -282,8 +282,6 @@ namespace osu.Game.Database /// /// A subscription token. It must be kept alive for as long as you want to receive change notifications. /// To stop receiving notifications, call . - /// - /// May be null in the case the provided collection is not managed. /// /// /// From ae2896ba7e9122d0ac3cd428d993430d1e18dcd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Jul 2023 22:08:48 +0200 Subject: [PATCH 0570/2100] Sprinkle some more null-forgiving operators --- osu.Game.Tests/Database/TestRealmKeyBindingStore.cs | 2 +- osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs | 2 +- osu.Game/Scoring/ScoreManager.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index f4467867db..e2774cef00 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Database realm.Run(innerRealm => { - var binding = innerRealm.ResolveReference(tsr); + var binding = innerRealm.ResolveReference(tsr)!; innerRealm.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace)); }); diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index b04e514ec2..725925c8cf 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -440,7 +440,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input } private void updateStoreFromButton(KeyButton button) => - realm.WriteAsync(r => r.Find(button.KeyBinding.ID).KeyCombinationString = button.KeyBinding.KeyCombinationString); + realm.WriteAsync(r => r.Find(button.KeyBinding.ID)!.KeyCombinationString = button.KeyBinding.KeyCombinationString); private void updateIsDefaultValue() { diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 55bcb9f79d..31b5bd8365 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -141,7 +141,7 @@ namespace osu.Game.Scoring { Realm.Run(r => { - var beatmapScores = r.Find(beatmap.ID).Scores.ToList(); + var beatmapScores = r.Find(beatmap.ID)!.Scores.ToList(); Delete(beatmapScores, silent); }); } From 8978f2ddd8c5cc366481dedde5a1a036abf383e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 10:09:24 +0900 Subject: [PATCH 0571/2100] Remove all usage of `!something!.something` aka don't mix NRT forgiving syntax with not syntax --- .../SongSelect/TestScenePlaySongSelect.cs | 20 ++++++++++--------- osu.Game/Database/RealmLive.cs | 3 ++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index af3a6e178c..0bff40f258 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -163,7 +163,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Key(Key.Enter); }); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddAssert("ensure selection changed", () => selected != Beatmap.Value); } @@ -186,7 +186,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Key(Key.Down); }); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddAssert("ensure selection didn't change", () => selected == Beatmap.Value); } @@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Key(Key.Enter); }); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddAssert("ensure selection changed", () => selected != Beatmap.Value); } @@ -244,7 +244,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.ReleaseButton(MouseButton.Left); }); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddAssert("ensure selection didn't change", () => selected == Beatmap.Value); } @@ -257,7 +257,7 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child"))); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddStep("return", () => songSelect!.MakeCurrent()); AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen()); @@ -275,7 +275,7 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child"))); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); @@ -292,7 +292,7 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child"))); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddStep("update beatmap", () => { @@ -1011,7 +1011,7 @@ namespace osu.Game.Tests.Visual.SongSelect }); }); - AddUntilStep("wait for results screen presented", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(getPresentBeatmap())); AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0); @@ -1040,7 +1040,7 @@ namespace osu.Game.Tests.Visual.SongSelect songSelect!.PresentScore(TestResources.CreateTestScoreInfo(getPresentBeatmap())); }); - AddUntilStep("wait for results screen presented", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(getPresentBeatmap())); AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0); @@ -1161,6 +1161,8 @@ namespace osu.Game.Tests.Visual.SongSelect rulesets.Dispose(); } + private void waitForDismissed() => AddUntilStep("wait for not current", () => !songSelect.AsNonNull().IsCurrentScreen()); + private partial class TestSongSelect : PlaySongSelect { public Action? StartRequested; diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index d96ccd9dcd..509fabec59 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using osu.Framework.Development; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Statistics; using Realms; @@ -133,7 +134,7 @@ namespace osu.Game.Database { Debug.Assert(ThreadSafety.IsUpdateThread); - if (dataIsFromUpdateThread && !data.Realm!.IsClosed) + if (dataIsFromUpdateThread && !data.Realm.AsNonNull().IsClosed) { RealmLiveStatistics.USAGE_UPDATE_IMMEDIATE.Value++; return; From d93548f4ea9a759faa6286e5814d54259f75d709 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 13:10:49 +0900 Subject: [PATCH 0572/2100] Add back "clear all" button for progress notifications but only clear cancelled --- osu.Game/Overlays/INotificationOverlay.cs | 2 +- osu.Game/Overlays/NotificationOverlay.cs | 4 +-- .../Notifications/NotificationSection.cs | 29 +++++++++---------- .../Notifications/ProgressNotification.cs | 2 ++ 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/INotificationOverlay.cs b/osu.Game/Overlays/INotificationOverlay.cs index c5ff10c619..7a44fd63ea 100644 --- a/osu.Game/Overlays/INotificationOverlay.cs +++ b/osu.Game/Overlays/INotificationOverlay.cs @@ -44,6 +44,6 @@ namespace osu.Game.Overlays /// /// All ongoing operations (ie. any not in a completed state). /// - public IEnumerable OngoingOperations => AllNotifications.OfType().Where(p => p.State != ProgressNotificationState.Completed && p.State != ProgressNotificationState.Cancelled); + public IEnumerable OngoingOperations => AllNotifications.OfType().Where(p => !p.CompletedOrCancelled); } } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 5c0427bf03..1ea0e6bb2e 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -108,8 +108,8 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.X, Children = new[] { - new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }, true), - new NotificationSection(NotificationsStrings.RunningTasks, new[] { typeof(ProgressNotification) }, false), + new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }), + new NotificationSection(NotificationsStrings.RunningTasks, new[] { typeof(ProgressNotification) }), } } } diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index 168c6afe27..80bc02a594 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -41,14 +41,11 @@ namespace osu.Game.Overlays.Notifications private readonly LocalisableString titleText; - private readonly bool allowClear; - - public NotificationSection(LocalisableString title, IEnumerable acceptedNotificationTypes, bool allowClear) + public NotificationSection(LocalisableString title, IEnumerable acceptedNotificationTypes) { AcceptedNotificationTypes = acceptedNotificationTypes.ToArray(); titleText = title; - this.allowClear = allowClear; } [BackgroundDependencyLoader] @@ -72,17 +69,15 @@ namespace osu.Game.Overlays.Notifications { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Children = new[] + Children = new Drawable[] { - allowClear - ? new ClearAllButton - { - Text = NotificationsStrings.ClearAll, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Action = clearAll - } - : Empty(), + new ClearAllButton + { + Text = NotificationsStrings.ClearAll, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Action = clearAll + }, new FillFlowContainer { Margin = new MarginPadding @@ -118,7 +113,11 @@ namespace osu.Game.Overlays.Notifications }); } - private void clearAll() => notifications.Children.ForEach(c => c.Close(true)); + private void clearAll() => notifications.Children.ForEach(c => + { + if (c is not ProgressNotification p || p.CompletedOrCancelled) + c.Close(true); + }); protected override void Update() { diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index e6662e2179..5df4c61fd7 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -25,6 +25,8 @@ namespace osu.Game.Overlays.Notifications public Func? CancelRequested { get; set; } + public bool CompletedOrCancelled => State == ProgressNotificationState.Completed || State == ProgressNotificationState.Cancelled; + protected override bool AllowFlingDismiss => false; /// From f8be6d41f71995e1705d0b9a192e162197f2fb37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 13:23:45 +0900 Subject: [PATCH 0573/2100] Play basic notification test first --- .../TestSceneNotificationOverlay.cs | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index 3cd5daf7a1..4d3ae079e3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -52,6 +52,32 @@ namespace osu.Game.Tests.Visual.UserInterface notificationOverlay.UnreadCount.ValueChanged += count => { displayedCount.Text = $"unread count: {count.NewValue}"; }; }); + [Test] + public void TestBasicFlow() + { + setState(Visibility.Visible); + AddStep(@"simple #1", sendHelloNotification); + AddStep(@"simple #2", sendAmazingNotification); + AddStep(@"progress #1", sendUploadProgress); + AddStep(@"progress #2", sendDownloadProgress); + + checkProgressingCount(2); + + setState(Visibility.Hidden); + + AddRepeatStep(@"add many simple", sendManyNotifications, 3); + + waitForCompletion(); + + AddStep(@"progress #3", sendUploadProgress); + + checkProgressingCount(1); + + checkDisplayedCount(33); + + waitForCompletion(); + } + [Test] public void TestForwardWithFlingRight() { @@ -411,32 +437,6 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("wait for update applied", () => applyUpdate); } - [Test] - public void TestBasicFlow() - { - setState(Visibility.Visible); - AddStep(@"simple #1", sendHelloNotification); - AddStep(@"simple #2", sendAmazingNotification); - AddStep(@"progress #1", sendUploadProgress); - AddStep(@"progress #2", sendDownloadProgress); - - checkProgressingCount(2); - - setState(Visibility.Hidden); - - AddRepeatStep(@"add many simple", sendManyNotifications, 3); - - waitForCompletion(); - - AddStep(@"progress #3", sendUploadProgress); - - checkProgressingCount(1); - - checkDisplayedCount(33); - - waitForCompletion(); - } - [Test] public void TestImportantWhileClosed() { From 04a15502152e90e603237f232cd53218e2dadc62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 13:32:15 +0900 Subject: [PATCH 0574/2100] Redesign "local input" toggle in manual input tests to be more user-friendly - Only displays when required (there's literally zero case we want to return input to the test, as this is automatic on next action) - No longer hugs the right side of the screen (blocking visibility of some tests). --- .../Visual/OsuManualInputManagerTestScene.cs | 55 ++++++++----------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index 16496ff320..d9c8170a33 100644 --- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Framework.Testing.Input; +using osu.Game.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2; @@ -25,9 +26,10 @@ namespace osu.Game.Tests.Visual protected readonly ManualInputManager InputManager; - private readonly RoundedButton buttonTest; private readonly RoundedButton buttonLocal; + private readonly Container takeControlOverlay; + /// /// Whether to create a nested container to handle s that result from local (manual) test input. /// This should be disabled when instantiating an instance else actions will be lost. @@ -66,12 +68,12 @@ namespace osu.Game.Tests.Visual UseParentInput = true, Child = mainContent }, - new Container + takeControlOverlay = new Container { AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Margin = new MarginPadding(5), + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Margin = new MarginPadding(40), CornerRadius = 5, Masking = true, Children = new Drawable[] @@ -80,28 +82,28 @@ namespace osu.Game.Tests.Visual { Colour = Color4.Black, RelativeSizeAxes = Axes.Both, - Alpha = 0.5f, + Alpha = 0.4f, }, new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Margin = new MarginPadding(5), - Spacing = new Vector2(5), + Margin = new MarginPadding(10), + Spacing = new Vector2(10), Children = new Drawable[] { new OsuSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = "Input Priority" + Font = OsuFont.Default.With(weight: FontWeight.Bold), + Text = "The test is currently overriding local input", }, new FillFlowContainer { AutoSizeAxes = Axes.Both, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Margin = new MarginPadding(5), Spacing = new Vector2(5), Direction = FillDirection.Horizontal, @@ -109,15 +111,9 @@ namespace osu.Game.Tests.Visual { buttonLocal = new RoundedButton { - Text = "local", - Size = new Vector2(50, 30), - Action = returnUserInput - }, - buttonTest = new RoundedButton - { - Text = "test", - Size = new Vector2(50, 30), - Action = returnTestInput + Text = "Take control back", + Size = new Vector2(180, 30), + Action = () => InputManager.UseParentInput = true }, } }, @@ -128,6 +124,13 @@ namespace osu.Game.Tests.Visual }); } + protected override void Update() + { + base.Update(); + + takeControlOverlay.Alpha = InputManager.UseParentInput ? 0 : 1; + } + /// /// Wait for a button to become enabled, then click it. /// @@ -146,19 +149,5 @@ namespace osu.Game.Tests.Visual InputManager.Click(MouseButton.Left); }); } - - protected override void Update() - { - base.Update(); - - buttonTest.Enabled.Value = InputManager.UseParentInput; - buttonLocal.Enabled.Value = !InputManager.UseParentInput; - } - - private void returnUserInput() => - InputManager.UseParentInput = true; - - private void returnTestInput() => - InputManager.UseParentInput = false; } } From d4c252ddf97c7edf9af02ab66d6a1ec78d7a74d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 14:28:57 +0900 Subject: [PATCH 0575/2100] Revert cancelling logic changes based on review feedback --- osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 7144fd8ca5..5c9a57724f 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -132,12 +132,14 @@ namespace osu.Game.Rulesets.Taiko.UI { leftCentreTrigger.StopAllPlayback(); rightCentreTrigger.StopAllPlayback(); + strongCentreTrigger.StopAllPlayback(); } private void flushRimTriggerSources() { leftRimTrigger.StopAllPlayback(); rightRimTrigger.StopAllPlayback(); + strongCentreTrigger.StopAllPlayback(); } public void OnReleased(KeyBindingReleaseEvent e) From 48f27ff340f3a417af63b4e323c9f9e2f9115e8f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 14:30:31 +0900 Subject: [PATCH 0576/2100] Move trigger source to own file Having such a large nested class inside a small top level class is VERY confusing. --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 118 ---------------- .../Argon/ArgonDrumSampleTriggerSource.cs | 129 ++++++++++++++++++ 2 files changed, 129 insertions(+), 118 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index 91393d99fe..7ce020f291 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -1,11 +1,8 @@ // 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 osu.Framework.Allocation; using osu.Game.Audio; -using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI; using osu.Game.Skinning; @@ -25,120 +22,5 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) => new ArgonDrumSampleTriggerSource(hitObjectContainer, balance); - - public partial class ArgonDrumSampleTriggerSource : DrumSampleTriggerSource - { - private readonly HitObjectContainer hitObjectContainer; - - [Resolved] - private ISkinSource skinSource { get; set; } = null!; - - /// - /// The minimum time to leave between flourishes that are added to strong rim hits. - /// - private const double time_between_flourishes = 2000; - - public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) - : base(hitObjectContainer, balance) - { - this.hitObjectContainer = hitObjectContainer; - } - - public override void Play(HitType hitType, bool strong) - { - TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; - - if (hitObject == null) - return; - - var originalSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); - - // If the sample is provided by a legacy skin, we should not try and do anything special. - if (skinSource.FindProvider(s => s.GetSample(originalSample) != null) is LegacySkinTransformer) - { - base.Play(hitType, strong); - return; - } - - // let the magic begin... - var samplesToPlay = new List { new VolumeAwareHitSampleInfo(originalSample, strong) }; - - if (strong && hitType == HitType.Rim && canPlayFlourish(hitObject)) - samplesToPlay.Add(new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(HitSampleInfo.HIT_FLOURISH), true)); - - PlaySamples(samplesToPlay.ToArray()); - } - - private bool canPlayFlourish(TaikoHitObject hitObject) - { - double? lastFlourish = null; - - var hitObjects = hitObjectContainer.AliveObjects - .Reverse() - .Select(d => d.HitObject) - .OfType() - .Where(h => h.IsStrong && h.Type == HitType.Rim); - - // Add an additional 'flourish' sample to strong rim hits (that are at least `time_between_flourishes` apart). - // This is applied to hitobjects in reverse order, as to sound more musically coherent by biasing towards to - // end of groups/combos of strong rim hits instead of the start. - foreach (var h in hitObjects) - { - bool canFlourish = lastFlourish == null || lastFlourish - h.StartTime >= time_between_flourishes; - - if (canFlourish) - lastFlourish = h.StartTime; - - if (h == hitObject) - return canFlourish; - } - - return false; - } - - public class VolumeAwareHitSampleInfo : HitSampleInfo - { - public const int SAMPLE_VOLUME_THRESHOLD_HARD = 90; - public const int SAMPLE_VOLUME_THRESHOLD_MEDIUM = 60; - - public VolumeAwareHitSampleInfo(HitSampleInfo sampleInfo, bool isStrong = false) - : base(sampleInfo.Name, isStrong ? BANK_STRONG : getBank(sampleInfo.Bank, sampleInfo.Name, sampleInfo.Volume), sampleInfo.Suffix, sampleInfo.Volume) - { - } - - public override IEnumerable LookupNames - { - get - { - foreach (string name in base.LookupNames) - yield return name.Insert(name.LastIndexOf('/') + 1, "Argon/taiko-"); - } - } - - private static string getBank(string originalBank, string sampleName, int volume) - { - // So basically we're overwriting mapper's bank intentions here. - // The rationale is that most taiko beatmaps only use a single bank, but regularly adjust volume. - - switch (sampleName) - { - case HIT_NORMAL: - case HIT_CLAP: - { - if (volume >= SAMPLE_VOLUME_THRESHOLD_HARD) - return BANK_DRUM; - - if (volume >= SAMPLE_VOLUME_THRESHOLD_MEDIUM) - return BANK_NORMAL; - - return BANK_SOFT; - } - - default: - return originalBank; - } - } - } - } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs new file mode 100644 index 0000000000..3454b0fd11 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs @@ -0,0 +1,129 @@ +// 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 osu.Framework.Allocation; +using osu.Game.Audio; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public partial class ArgonDrumSampleTriggerSource : DrumSampleTriggerSource + { + private readonly HitObjectContainer hitObjectContainer; + + [Resolved] + private ISkinSource skinSource { get; set; } = null!; + + /// + /// The minimum time to leave between flourishes that are added to strong rim hits. + /// + private const double time_between_flourishes = 2000; + + public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) + : base(hitObjectContainer, balance) + { + this.hitObjectContainer = hitObjectContainer; + } + + public override void Play(HitType hitType, bool strong) + { + TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; + + if (hitObject == null) + return; + + var originalSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); + + // If the sample is provided by a legacy skin, we should not try and do anything special. + if (skinSource.FindProvider(s => s.GetSample(originalSample) != null) is LegacySkinTransformer) + { + base.Play(hitType, strong); + return; + } + + // let the magic begin... + var samplesToPlay = new List { new VolumeAwareHitSampleInfo(originalSample, strong) }; + + if (strong && hitType == HitType.Rim && canPlayFlourish(hitObject)) + samplesToPlay.Add(new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(HitSampleInfo.HIT_FLOURISH), true)); + + PlaySamples(samplesToPlay.ToArray()); + } + + private bool canPlayFlourish(TaikoHitObject hitObject) + { + double? lastFlourish = null; + + var hitObjects = hitObjectContainer.AliveObjects + .Reverse() + .Select(d => d.HitObject) + .OfType() + .Where(h => h.IsStrong && h.Type == HitType.Rim); + + // Add an additional 'flourish' sample to strong rim hits (that are at least `time_between_flourishes` apart). + // This is applied to hitobjects in reverse order, as to sound more musically coherent by biasing towards to + // end of groups/combos of strong rim hits instead of the start. + foreach (var h in hitObjects) + { + bool canFlourish = lastFlourish == null || lastFlourish - h.StartTime >= time_between_flourishes; + + if (canFlourish) + lastFlourish = h.StartTime; + + if (h == hitObject) + return canFlourish; + } + + return false; + } + + public class VolumeAwareHitSampleInfo : HitSampleInfo + { + public const int SAMPLE_VOLUME_THRESHOLD_HARD = 90; + public const int SAMPLE_VOLUME_THRESHOLD_MEDIUM = 60; + + public VolumeAwareHitSampleInfo(HitSampleInfo sampleInfo, bool isStrong = false) + : base(sampleInfo.Name, isStrong ? BANK_STRONG : getBank(sampleInfo.Bank, sampleInfo.Name, sampleInfo.Volume), sampleInfo.Suffix, sampleInfo.Volume) + { + } + + public override IEnumerable LookupNames + { + get + { + foreach (string name in base.LookupNames) + yield return name.Insert(name.LastIndexOf('/') + 1, "Argon/taiko-"); + } + } + + private static string getBank(string originalBank, string sampleName, int volume) + { + // So basically we're overwriting mapper's bank intentions here. + // The rationale is that most taiko beatmaps only use a single bank, but regularly adjust volume. + + switch (sampleName) + { + case HIT_NORMAL: + case HIT_CLAP: + { + if (volume >= SAMPLE_VOLUME_THRESHOLD_HARD) + return BANK_DRUM; + + if (volume >= SAMPLE_VOLUME_THRESHOLD_MEDIUM) + return BANK_NORMAL; + + return BANK_SOFT; + } + + default: + return originalBank; + } + } + } + } +} From 6bfbcca2fdf0a1b1bf6342b852aad99ec2ce4788 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 14:44:44 +0900 Subject: [PATCH 0577/2100] Move `VolumeAwareHitSampleInfo` to own file --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 6 +-- .../Argon/ArgonDrumSampleTriggerSource.cs | 44 ---------------- .../Argon/VolumeAwareHitSampleInfo.cs | 52 +++++++++++++++++++ 3 files changed, 55 insertions(+), 47 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index 7ce020f291..898753b568 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -15,9 +15,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon private void load(IPooledSampleProvider sampleProvider) { // Warm up pools for non-standard samples. - sampleProvider.GetPooledSample(new ArgonDrumSampleTriggerSource.VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_NORMAL), true)); - sampleProvider.GetPooledSample(new ArgonDrumSampleTriggerSource.VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_CLAP), true)); - sampleProvider.GetPooledSample(new ArgonDrumSampleTriggerSource.VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_FLOURISH), true)); + sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_NORMAL), true)); + sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_CLAP), true)); + sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_FLOURISH), true)); } protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) => diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs index 3454b0fd11..958d4e3aec 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs @@ -81,49 +81,5 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon return false; } - - public class VolumeAwareHitSampleInfo : HitSampleInfo - { - public const int SAMPLE_VOLUME_THRESHOLD_HARD = 90; - public const int SAMPLE_VOLUME_THRESHOLD_MEDIUM = 60; - - public VolumeAwareHitSampleInfo(HitSampleInfo sampleInfo, bool isStrong = false) - : base(sampleInfo.Name, isStrong ? BANK_STRONG : getBank(sampleInfo.Bank, sampleInfo.Name, sampleInfo.Volume), sampleInfo.Suffix, sampleInfo.Volume) - { - } - - public override IEnumerable LookupNames - { - get - { - foreach (string name in base.LookupNames) - yield return name.Insert(name.LastIndexOf('/') + 1, "Argon/taiko-"); - } - } - - private static string getBank(string originalBank, string sampleName, int volume) - { - // So basically we're overwriting mapper's bank intentions here. - // The rationale is that most taiko beatmaps only use a single bank, but regularly adjust volume. - - switch (sampleName) - { - case HIT_NORMAL: - case HIT_CLAP: - { - if (volume >= SAMPLE_VOLUME_THRESHOLD_HARD) - return BANK_DRUM; - - if (volume >= SAMPLE_VOLUME_THRESHOLD_MEDIUM) - return BANK_NORMAL; - - return BANK_SOFT; - } - - default: - return originalBank; - } - } - } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.cs new file mode 100644 index 0000000000..3ca4b5a3c7 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.cs @@ -0,0 +1,52 @@ +// 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 osu.Game.Audio; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public class VolumeAwareHitSampleInfo : HitSampleInfo + { + public const int SAMPLE_VOLUME_THRESHOLD_HARD = 90; + public const int SAMPLE_VOLUME_THRESHOLD_MEDIUM = 60; + + public VolumeAwareHitSampleInfo(HitSampleInfo sampleInfo, bool isStrong = false) + : base(sampleInfo.Name, isStrong ? BANK_STRONG : getBank(sampleInfo.Bank, sampleInfo.Name, sampleInfo.Volume), sampleInfo.Suffix, sampleInfo.Volume) + { + } + + public override IEnumerable LookupNames + { + get + { + foreach (string name in base.LookupNames) + yield return name.Insert(name.LastIndexOf('/') + 1, "Argon/taiko-"); + } + } + + private static string getBank(string originalBank, string sampleName, int volume) + { + // So basically we're overwriting mapper's bank intentions here. + // The rationale is that most taiko beatmaps only use a single bank, but regularly adjust volume. + + switch (sampleName) + { + case HIT_NORMAL: + case HIT_CLAP: + { + if (volume >= SAMPLE_VOLUME_THRESHOLD_HARD) + return BANK_DRUM; + + if (volume >= SAMPLE_VOLUME_THRESHOLD_MEDIUM) + return BANK_NORMAL; + + return BANK_SOFT; + } + + default: + return originalBank; + } + } + } +} From 9bdc80a74934b781216ff924d5ccca79068e467e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 14:46:36 +0900 Subject: [PATCH 0578/2100] Move flourish playback to own trigger source --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 18 ++++- .../Argon/ArgonDrumSampleTriggerSource.cs | 39 ---------- .../Argon/ArgonFlourishTriggerSource.cs | 76 +++++++++++++++++++ .../UI/DrumSamplePlayer.cs | 5 +- 4 files changed, 97 insertions(+), 41 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index 898753b568..2ff36ef9bf 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Game.Audio; +using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI; using osu.Game.Skinning; @@ -11,16 +12,31 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { internal partial class ArgonDrumSamplePlayer : DrumSamplePlayer { + private ArgonFlourishTriggerSource argonFlourishTrigger = null!; + [BackgroundDependencyLoader] - private void load(IPooledSampleProvider sampleProvider) + private void load(Playfield playfield, IPooledSampleProvider sampleProvider) { + var hitObjectContainer = playfield.HitObjectContainer; + // Warm up pools for non-standard samples. sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_NORMAL), true)); sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_CLAP), true)); sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_FLOURISH), true)); + + // We want to play back flourishes in an isolated source as to not have them cancelled. + AddInternal(argonFlourishTrigger = new ArgonFlourishTriggerSource(hitObjectContainer)); } protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) => new ArgonDrumSampleTriggerSource(hitObjectContainer, balance); + + protected override void Play(DrumSampleTriggerSource triggerSource, HitType hitType, bool strong) + { + base.Play(triggerSource, hitType, strong); + + // This won't always play something, but the logic for flourish playback is contained within. + argonFlourishTrigger.Play(hitType, strong); + } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs index 958d4e3aec..fb4c1067e3 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Game.Audio; using osu.Game.Rulesets.Taiko.Objects; @@ -14,20 +13,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { public partial class ArgonDrumSampleTriggerSource : DrumSampleTriggerSource { - private readonly HitObjectContainer hitObjectContainer; - [Resolved] private ISkinSource skinSource { get; set; } = null!; - /// - /// The minimum time to leave between flourishes that are added to strong rim hits. - /// - private const double time_between_flourishes = 2000; - public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) : base(hitObjectContainer, balance) { - this.hitObjectContainer = hitObjectContainer; } public override void Play(HitType hitType, bool strong) @@ -49,37 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon // let the magic begin... var samplesToPlay = new List { new VolumeAwareHitSampleInfo(originalSample, strong) }; - if (strong && hitType == HitType.Rim && canPlayFlourish(hitObject)) - samplesToPlay.Add(new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(HitSampleInfo.HIT_FLOURISH), true)); - PlaySamples(samplesToPlay.ToArray()); } - - private bool canPlayFlourish(TaikoHitObject hitObject) - { - double? lastFlourish = null; - - var hitObjects = hitObjectContainer.AliveObjects - .Reverse() - .Select(d => d.HitObject) - .OfType() - .Where(h => h.IsStrong && h.Type == HitType.Rim); - - // Add an additional 'flourish' sample to strong rim hits (that are at least `time_between_flourishes` apart). - // This is applied to hitobjects in reverse order, as to sound more musically coherent by biasing towards to - // end of groups/combos of strong rim hits instead of the start. - foreach (var h in hitObjects) - { - bool canFlourish = lastFlourish == null || lastFlourish - h.StartTime >= time_between_flourishes; - - if (canFlourish) - lastFlourish = h.StartTime; - - if (h == hitObject) - return canFlourish; - } - - return false; - } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs new file mode 100644 index 0000000000..661f737843 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs @@ -0,0 +1,76 @@ +// 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 osu.Framework.Allocation; +using osu.Game.Audio; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + internal partial class ArgonFlourishTriggerSource : ArgonDrumSampleTriggerSource + { + private readonly HitObjectContainer hitObjectContainer; + + [Resolved] + private ISkinSource skinSource { get; set; } = null!; + + /// + /// The minimum time to leave between flourishes that are added to strong rim hits. + /// + private const double time_between_flourishes = 2000; + + public ArgonFlourishTriggerSource(HitObjectContainer hitObjectContainer) + : base(hitObjectContainer, SampleBalance.Centre) + { + this.hitObjectContainer = hitObjectContainer; + } + + public override void Play(HitType hitType, bool strong) + { + TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; + + if (hitObject == null) + return; + + var originalSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); + + // If the sample is provided by a legacy skin, we should not try and do anything special. + if (skinSource.FindProvider(s => s.GetSample(originalSample) != null) is LegacySkinTransformer) + return; + + if (strong && hitType == HitType.Rim && canPlayFlourish(hitObject)) + PlaySamples(new ISampleInfo[] { new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(HitSampleInfo.HIT_FLOURISH), true) }); + } + + private bool canPlayFlourish(TaikoHitObject hitObject) + { + double? lastFlourish = null; + + var hitObjects = hitObjectContainer.AliveObjects + .Reverse() + .Select(d => d.HitObject) + .OfType() + .Where(h => h.IsStrong && h.Type == HitType.Rim); + + // Add an additional 'flourish' sample to strong rim hits (that are at least `time_between_flourishes` apart). + // This is applied to hitobjects in reverse order, as to sound more musically coherent by biasing towards to + // end of groups/combos of strong rim hits instead of the start. + foreach (var h in hitObjects) + { + bool canFlourish = lastFlourish == null || lastFlourish - h.StartTime >= time_between_flourishes; + + if (canFlourish) + lastFlourish = h.StartTime; + + if (h == hitObject) + return canFlourish; + } + + return false; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 5c9a57724f..310a4c1edb 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Taiko.UI } } - triggerSource.Play(hitType, strong); + Play(triggerSource, hitType, strong); lastHitTime = Time.Current; lastAction = e.Action; @@ -101,6 +101,9 @@ namespace osu.Game.Rulesets.Taiko.UI return false; } + protected virtual void Play(DrumSampleTriggerSource triggerSource, HitType hitType, bool strong) => + triggerSource.Play(hitType, strong); + private bool checkStrongValidity(TaikoAction newAction, TaikoAction? lastAction, double timeBetweenActions) { if (lastAction == null) From 755b82a30883d6922f1439bd6cb45383085d1946 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 14:54:49 +0900 Subject: [PATCH 0579/2100] Implement flourish trigger source via base class --- .../Skinning/Argon/ArgonFlourishTriggerSource.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs index 661f737843..728977547c 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs @@ -11,7 +11,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Skinning.Argon { - internal partial class ArgonFlourishTriggerSource : ArgonDrumSampleTriggerSource + internal partial class ArgonFlourishTriggerSource : DrumSampleTriggerSource { private readonly HitObjectContainer hitObjectContainer; @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon private const double time_between_flourishes = 2000; public ArgonFlourishTriggerSource(HitObjectContainer hitObjectContainer) - : base(hitObjectContainer, SampleBalance.Centre) + : base(hitObjectContainer) { this.hitObjectContainer = hitObjectContainer; } From 8b5d5c9ae2497a9dc24a932b65ec33db875fcdc0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 15:13:25 +0900 Subject: [PATCH 0580/2100] Fix rewinding causing incorrectly stronged non-strong hits --- osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 310a4c1edb..08bde9a316 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Taiko.UI if (lastAction == null) return false; - if (timeBetweenActions > DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW) + if (timeBetweenActions < 0 || timeBetweenActions > DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW) return false; switch (newAction) From e0fc97bb9302d3bb5d476b5af8936da1b91e13ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 15:21:24 +0900 Subject: [PATCH 0581/2100] Replace various local implementations of rewinding checks with new property --- .../Skinning/Legacy/LegacyCatchComboCounter.cs | 3 ++- .../Objects/Drawables/DrawableHoldNote.cs | 3 ++- .../Objects/Drawables/DrawableSliderBall.cs | 6 ++---- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 3 ++- osu.Game/Screens/Play/ComboEffects.cs | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs index 55b24b3ffa..f38b9b430e 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Catch.UI; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -59,7 +60,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy lastDisplayedCombo = combo; - if (Time.Elapsed < 0) + if ((Clock as IGameplayClock)?.IsRewinding == true) { // needs more work to make rewind somehow look good. // basically we want the previous increment to play... or turning off RemoveCompletedTransforms (not feasible from a performance angle). diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index a8563d65c4..d5e212d389 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; @@ -298,7 +299,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables return false; // do not run any of this logic when rewinding, as it inverts order of presses/releases. - if (Time.Elapsed < 0) + if ((Clock as IGameplayClock)?.IsRewinding == true) return false; if (CheckHittable?.Invoke(this, Time.Current) == false) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs index e1766adc20..d06fb5b4de 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs @@ -14,6 +14,7 @@ using osu.Framework.Input.Events; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; @@ -179,16 +180,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Vector2? lastPosition; - private bool rewinding; - public void UpdateProgress(double completionProgress) { Position = drawableSlider.HitObject.CurvePositionAt(completionProgress); var diff = lastPosition.HasValue ? lastPosition.Value - Position : Position - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f); - if (Clock.ElapsedFrameTime != 0) - rewinding = Clock.ElapsedFrameTime < 0; + bool rewinding = (Clock as IGameplayClock)?.IsRewinding == true; // Ensure the value is substantially high enough to allow for Atan2 to get a valid angle. if (diff.LengthFast < 0.01f) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index e4d8eb2335..bf649a0a15 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -24,6 +24,7 @@ using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK.Graphics; @@ -688,7 +689,7 @@ namespace osu.Game.Rulesets.Objects.Drawables protected bool UpdateResult(bool userTriggered) { // It's possible for input to get into a bad state when rewinding gameplay, so results should not be processed - if (Time.Elapsed < 0) + if ((Clock as IGameplayClock)?.IsRewinding == true) return false; if (Judged) diff --git a/osu.Game/Screens/Play/ComboEffects.cs b/osu.Game/Screens/Play/ComboEffects.cs index 09c94a8f1d..6f12cfde64 100644 --- a/osu.Game/Screens/Play/ComboEffects.cs +++ b/osu.Game/Screens/Play/ComboEffects.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Play if (gameplayClock.CurrentTime < firstBreakTime) firstBreakTime = null; - if (gameplayClock.ElapsedFrameTime < 0) + if (gameplayClock.IsRewinding) return; if (combo.NewValue == 0 && (combo.OldValue > 20 || (alwaysPlayFirst.Value && firstBreakTime == null))) From 67746e1369bacd864c7b107724416e656119eac4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 15:36:17 +0900 Subject: [PATCH 0582/2100] Move retry sample playback to `PlayerLoader` --- osu.Game/Screens/Play/Player.cs | 4 ---- osu.Game/Screens/Play/PlayerLoader.cs | 9 ++++++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2cb7748a15..379c10a4a4 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -112,8 +112,6 @@ namespace osu.Game.Screens.Play private Ruleset ruleset; - private SkinnableSound sampleRestart; - public BreakOverlay BreakOverlay; /// @@ -303,7 +301,6 @@ namespace osu.Game.Screens.Play Restart(true); }, }, - sampleRestart = new SkinnableSound(new SampleInfo(@"Gameplay/restart", @"pause-retry-click")) }); } @@ -673,7 +670,6 @@ namespace osu.Game.Screens.Play // stopping here is to ensure music doesn't become audible after exiting back to PlayerLoader. musicController.Stop(); - sampleRestart?.Play(); RestartRequested?.Invoke(quickRestart); PerformExit(false); diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 30ae5ee5aa..4b15bac0f3 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Transforms; using osu.Framework.Input; using osu.Framework.Screens; using osu.Framework.Threading; +using osu.Game.Audio; using osu.Game.Audio.Effects; using osu.Game.Configuration; using osu.Game.Graphics; @@ -25,6 +26,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Menu; using osu.Game.Screens.Play.PlayerSettings; +using osu.Game.Skinning; using osu.Game.Users; using osu.Game.Utils; using osuTK; @@ -76,6 +78,8 @@ namespace osu.Game.Screens.Play private AudioFilter lowPassFilter = null!; private AudioFilter highPassFilter = null!; + private SkinnableSound sampleRestart = null!; + [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); @@ -199,7 +203,8 @@ namespace osu.Game.Screens.Play }, idleTracker = new IdleTracker(750), lowPassFilter = new AudioFilter(audio.TrackMixer), - highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass) + highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass), + sampleRestart = new SkinnableSound(new SampleInfo(@"Gameplay/restart", @"pause-retry-click")) }; if (Beatmap.Value.BeatmapInfo.EpilepsyWarning) @@ -265,6 +270,8 @@ namespace osu.Game.Screens.Play playerConsumed = false; cancelLoad(); + sampleRestart.Play(); + contentIn(); } From 9732e5733c2685f926e08a3300e246a2b32c59e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 15:45:21 +0900 Subject: [PATCH 0583/2100] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index b4d8dd513f..d30398a475 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From ba0ab7383d051a074fc59fa2416f103093102e17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 16:28:49 +0900 Subject: [PATCH 0584/2100] Fix broken test nullability --- osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs index f377d92911..d30b3c089e 100644 --- a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs @@ -383,7 +383,7 @@ namespace osu.Game.Tests.Database beatmapInfo.Hash = new_beatmap_hash; beatmapInfo.ResetOnlineInfo(); - beatmapInfo.UpdateLocalScores(s.Realm); + beatmapInfo.UpdateLocalScores(s.Realm!); }); realm.Run(r => r.Refresh()); From 82babbf8faee0876962df4f93f08443c91e6091f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 17:32:22 +0900 Subject: [PATCH 0585/2100] Adjust results screen transition tweens to feel better --- osu.Game/Screens/Ranking/ResultsScreen.cs | 8 ++++---- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index b9f3b65129..96fed3e6ba 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -305,7 +305,7 @@ namespace osu.Game.Screens.Ranking float origLocation = detachedPanelContainer.ToLocalSpace(screenSpacePos).X; expandedPanel.MoveToX(origLocation) .Then() - .MoveToX(StatisticsPanel.SIDE_PADDING, 150, Easing.OutQuint); + .MoveToX(StatisticsPanel.SIDE_PADDING, 400, Easing.OutElasticQuarter); // Hide contracted panels. foreach (var contracted in ScorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted)) @@ -313,7 +313,7 @@ namespace osu.Game.Screens.Ranking ScorePanelList.HandleInput = false; // Dim background. - ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.1f), 150)); + ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.4f), 400, Easing.OutQuint)); detachedPanel = expandedPanel; } @@ -329,7 +329,7 @@ namespace osu.Game.Screens.Ranking float origLocation = detachedPanel.Parent.ToLocalSpace(screenSpacePos).X; detachedPanel.MoveToX(origLocation) .Then() - .MoveToX(0, 150, Easing.OutQuint); + .MoveToX(0, 250, Easing.OutElasticQuarter); // Show contracted panels. foreach (var contracted in ScorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted)) @@ -337,7 +337,7 @@ namespace osu.Game.Screens.Ranking ScorePanelList.HandleInput = true; // Un-dim background. - ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.5f), 150)); + ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.5f), 250, Easing.OutQuint)); detachedPanel = null; } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index c36d7726dc..8b059efaf4 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -223,7 +223,7 @@ namespace osu.Game.Screens.Ranking.Statistics protected override void PopIn() { - this.FadeIn(150, Easing.OutQuint); + this.FadeIn(350, Easing.OutQuint); popInSample?.Play(); wasOpened = true; @@ -231,7 +231,7 @@ namespace osu.Game.Screens.Ranking.Statistics protected override void PopOut() { - this.FadeOut(150, Easing.OutQuint); + this.FadeOut(250, Easing.OutQuint); if (wasOpened) popOutSample?.Play(); From 0e85a33ca2d6ab741566879c336246de01caaa71 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 20:58:04 +0900 Subject: [PATCH 0586/2100] Update framework --- 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 fdec4e575b..270a0aa44b 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -11,7 +11,7 @@ manifestmerger.jar - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 20b1574617..184a77a286 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 86694e268a..51bcc36621 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -16,6 +16,6 @@ iossimulator-x64 - + From 2cd5a4c6c26f5c6f0278a5aec17a6e20bdd28b89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 22:38:07 +0900 Subject: [PATCH 0587/2100] Add generated code hints in editorconfig / dotsettings --- .editorconfig | 3 +++ osu.sln.DotSettings | 1 + 2 files changed, 4 insertions(+) diff --git a/.editorconfig b/.editorconfig index 67c47000d3..c249e5e9b3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,6 +9,9 @@ indent_style = space indent_size = 2 trim_trailing_whitespace = true +[g_*.cs] +generated_code = true + [*.cs] end_of_line = crlf insert_final_newline = true diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index b54794cd6d..482095db57 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -5,6 +5,7 @@ True ExplicitlyExcluded ExplicitlyExcluded + g_*.cs SOLUTION WARNING WARNING From a76cd9b0e6d579e4aa67e796f1d1cb7457f857f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 8 Jul 2023 00:44:26 +0900 Subject: [PATCH 0588/2100] Update osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs Co-authored-by: Jamie Taylor --- osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 08bde9a316..93d2406c0c 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.Taiko.UI { leftRimTrigger.StopAllPlayback(); rightRimTrigger.StopAllPlayback(); - strongCentreTrigger.StopAllPlayback(); + strongRimTrigger.StopAllPlayback(); } public void OnReleased(KeyBindingReleaseEvent e) From 9dae80673474d40902c76f2149afa378d01e7699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 8 Jul 2023 13:31:21 +0200 Subject: [PATCH 0589/2100] Use `IsRewinding` in a few more places --- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 3 ++- osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 1b99270b65..567c288b47 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Rulesets.Catch.UI @@ -96,7 +97,7 @@ namespace osu.Game.Rulesets.Catch.UI comboDisplay.X = Catcher.X; - if (Time.Elapsed <= 0) + if ((Clock as IGameplayClock)?.IsRewinding == true) { // This is probably a wrong value, but currently the true value is not recorded. // Setting `true` will prevent generation of false-positive after-images (with more false-negatives). diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index d5e212d389..c3fec92b92 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -338,7 +338,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables return; // do not run any of this logic when rewinding, as it inverts order of presses/releases. - if (Time.Elapsed < 0) + if ((Clock as IGameplayClock)?.IsRewinding == true) return; Tail.UpdateResult(); From 3a9b259f8a34100ab4ef796cfa3f99aef925129e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 8 Jul 2023 14:10:05 +0200 Subject: [PATCH 0590/2100] Add back removed `.ToUpper()` case transform --- osu.Game/Overlays/Notifications/NotificationSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index 80bc02a594..be57d23446 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -73,7 +73,7 @@ namespace osu.Game.Overlays.Notifications { new ClearAllButton { - Text = NotificationsStrings.ClearAll, + Text = NotificationsStrings.ClearAll.ToUpper(), Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Action = clearAll From cdaf8e4b0ff53937209603f30d8b46b290dac83a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 8 Jul 2023 14:11:58 +0200 Subject: [PATCH 0591/2100] Flip and rename `CompletedOrCancelled` to `Ongoing` --- osu.Game/Overlays/INotificationOverlay.cs | 4 ++-- osu.Game/Overlays/Notifications/NotificationSection.cs | 2 +- osu.Game/Overlays/Notifications/ProgressNotification.cs | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/INotificationOverlay.cs b/osu.Game/Overlays/INotificationOverlay.cs index 7a44fd63ea..19c646a714 100644 --- a/osu.Game/Overlays/INotificationOverlay.cs +++ b/osu.Game/Overlays/INotificationOverlay.cs @@ -42,8 +42,8 @@ namespace osu.Game.Overlays IEnumerable AllNotifications { get; } /// - /// All ongoing operations (ie. any not in a completed state). + /// All ongoing operations (ie. any not in a completed or cancelled state). /// - public IEnumerable OngoingOperations => AllNotifications.OfType().Where(p => !p.CompletedOrCancelled); + public IEnumerable OngoingOperations => AllNotifications.OfType().Where(p => p.Ongoing); } } diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index be57d23446..10c2900d63 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -115,7 +115,7 @@ namespace osu.Game.Overlays.Notifications private void clearAll() => notifications.Children.ForEach(c => { - if (c is not ProgressNotification p || p.CompletedOrCancelled) + if (c is not ProgressNotification p || !p.Ongoing) c.Close(true); }); diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 5df4c61fd7..466dfab5c5 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -25,7 +25,10 @@ namespace osu.Game.Overlays.Notifications public Func? CancelRequested { get; set; } - public bool CompletedOrCancelled => State == ProgressNotificationState.Completed || State == ProgressNotificationState.Cancelled; + /// + /// Whether the operation represented by the is still ongoing. + /// + public bool Ongoing => State != ProgressNotificationState.Completed && State != ProgressNotificationState.Cancelled; protected override bool AllowFlingDismiss => false; From 0ecfb7b36fbf6389f433ff2234a92936b24d3eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 8 Jul 2023 15:03:33 +0200 Subject: [PATCH 0592/2100] Remove unused field --- osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index d9c8170a33..37260b3b13 100644 --- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs @@ -26,8 +26,6 @@ namespace osu.Game.Tests.Visual protected readonly ManualInputManager InputManager; - private readonly RoundedButton buttonLocal; - private readonly Container takeControlOverlay; /// @@ -109,7 +107,7 @@ namespace osu.Game.Tests.Visual Children = new Drawable[] { - buttonLocal = new RoundedButton + new RoundedButton { Text = "Take control back", Size = new Vector2(180, 30), From fee56ac6d2ece7d897a6419d0bfbc9ee65e95c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 8 Jul 2023 18:29:49 +0200 Subject: [PATCH 0593/2100] Use new `IGameplayClock.IsRewinding` member --- osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 93d2406c0c..57067ac666 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -9,6 +9,7 @@ using osu.Framework.Input.Events; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; namespace osu.Game.Rulesets.Taiko.UI { @@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.UI public bool OnPressed(KeyBindingPressEvent e) { - if (Time.Elapsed < 0) + if ((Clock as IGameplayClock)?.IsRewinding == true) return false; HitType hitType; From ba0cd7a3f27206e5b815b709da91bf46969e3c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 8 Jul 2023 19:22:32 +0200 Subject: [PATCH 0594/2100] Fix flourish sample not playing if strong hits are hit early --- .../Skinning/Argon/ArgonFlourishTriggerSource.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs index 728977547c..8dfe31b55d 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs @@ -66,7 +66,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon if (canFlourish) lastFlourish = h.StartTime; - if (h == hitObject) + // hitObject can be either the strong hit itself (if hit late), or its nested strong object (if hit early) + // due to `GetMostValidObject()` idiosyncrasies. + // whichever it is, if we encounter it during iteration, stop looking. + if (h == hitObject || h.NestedHitObjects.Contains(hitObject)) return canFlourish; } From e9ecad983932086155adb7bb2527ab0f5bf37f3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 8 Jul 2023 23:02:41 +0200 Subject: [PATCH 0595/2100] Add failing test cases covering NaN-timing-point sliders --- .../OsuBeatmapConversionTest.cs | 1 + .../OsuDifficultyCalculatorTest.cs | 1 + .../nan-slider-expected-conversion.json | 1 + .../Resources/Testing/Beatmaps/nan-slider.osu | 18 ++++++++++++++++++ 4 files changed, 21 insertions(+) create mode 100755 osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider-expected-conversion.json create mode 100755 osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider.osu diff --git a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs index 4c11efcc7c..b94e9f38c6 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs @@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Tests [TestCase("uneven-repeat-slider")] [TestCase("old-stacking")] [TestCase("multi-segment-slider")] + [TestCase("nan-slider")] public void Test(string name) => base.Test(name); protected override IEnumerable CreateConvertValue(HitObject hitObject) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index cda330afe5..9c6449cfa9 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Osu.Tests [TestCase(6.7115569159190587d, 206, "diffcalc-test")] [TestCase(1.4391311903612753d, 45, "zero-length-sliders")] + [TestCase(0.14102693012101306d, 1, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider-expected-conversion.json new file mode 100755 index 0000000000..86a4a278f1 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":77497.0,"Objects":[{"StartTime":77497.0,"EndTime":77497.0,"X":298.0,"Y":290.0},{"StartTime":77533.0,"EndTime":77533.0,"X":276.162567,"Y":293.0336}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider.osu new file mode 100755 index 0000000000..fa545a7614 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider.osu @@ -0,0 +1,18 @@ +osu file format v14 + +[Difficulty] +HPDrainRate:5.8 +CircleSize:4 +OverallDifficulty:9.6 +ApproachRate:10 +SliderMultiplier:2 +SliderTickRate:1 + +[TimingPoints] +77211,-100,4,3,50,70,0,0 +77497,8.40402703648439,4,3,51,70,1,8 +77497,NaN,4,3,51,70,0,8 +77498,285.714285714286,4,3,51,70,1,0 + +[HitObjects] +298,290,77497,6,0,B|234:298|192:279|192:279|180:299|180:299|205:311|238:318|238:318|230:347|217:371|217:371|137:370|80:340|80:340|65:259|73:143|102:68|102:68|149:49|199:34|199:34|213:54|213:54|267:38|324:40|324:40|332:18|332:18|385:20|435:27|435:27|480:93|517:204|521:286|521:286|474:329|396:350|396:350|377:329|363:302|363:302|393:287|415:271|415:271|398:254|398:254|362:282|299:290,1,1723.66345596313,10|0,1:0|3:0,3:0:0:0: From 56a2ba4ac0291ade389d9dbfe5dbb801e70d04d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 8 Jul 2023 23:08:36 +0200 Subject: [PATCH 0596/2100] Fix `GenerateTicks` being lost during osu! beatmap conversion process --- osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs index e947690668..790af6cfc1 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu /// /// Legacy osu! Slider-type, used for parsing Beatmaps. /// - internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition, IHasCombo + internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition, IHasCombo, IHasGenerateTicks { public Vector2 Position { get; set; } @@ -20,5 +20,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public bool NewCombo { get; set; } public int ComboOffset { get; set; } + + public bool GenerateTicks { get; set; } = true; } } From ae05df3b8c5d0f44bb1de43c3dbb8087d90adc35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 15:56:51 +0200 Subject: [PATCH 0597/2100] Add `ModScoreV2` --- osu.Game/Rulesets/Mods/ModScoreV2.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 osu.Game/Rulesets/Mods/ModScoreV2.cs diff --git a/osu.Game/Rulesets/Mods/ModScoreV2.cs b/osu.Game/Rulesets/Mods/ModScoreV2.cs new file mode 100644 index 0000000000..6d56b2d86f --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModScoreV2.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// This mod is used strictly to mark osu!stable scores set with the "Score V2" mod active. + /// It should not be used in any real capacity going forward. + /// + public class ModScoreV2 : Mod + { + public override string Name => "Score V2"; + public override string Acronym => @"SV2"; + public override ModType Type => ModType.System; + public override LocalisableString Description => "Score set on earlier osu! versions with the V2 scoring algorithm active."; + public override double ScoreMultiplier => 1; + } +} From 10ba04512d5963eb1ae6d028d83d65dd644d8ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 16:06:05 +0200 Subject: [PATCH 0598/2100] Add `ScoreV2` to `LegacyMods` --- osu.Game/Beatmaps/Legacy/LegacyMods.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Beatmaps/Legacy/LegacyMods.cs b/osu.Game/Beatmaps/Legacy/LegacyMods.cs index 0e517ea3df..747015d90a 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyMods.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyMods.cs @@ -38,6 +38,7 @@ namespace osu.Game.Beatmaps.Legacy Key1 = 1 << 26, Key3 = 1 << 27, Key2 = 1 << 28, + ScoreV2 = 1 << 29, Mirror = 1 << 30, } } From 2cd5fd5944e48e9625353e3295ec017e1399ab73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 16:09:25 +0200 Subject: [PATCH 0599/2100] Add failing legacy mod conversion test cases --- osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs | 4 +++- osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs | 4 +++- osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs | 4 +++- osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs index b74120fa3c..dacfd649ef 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs @@ -5,6 +5,7 @@ using System; using NUnit.Framework; using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Mods; using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Catch.Tests @@ -24,7 +25,8 @@ namespace osu.Game.Rulesets.Catch.Tests new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } }, new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } }, new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } }, - new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } } + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } }, + new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } }, }; [TestCaseSource(nameof(catch_mod_mapping))] diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs index 3a9639e04d..cb2abc1595 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs @@ -5,6 +5,7 @@ using System; using NUnit.Framework; using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mods; using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Mania.Tests @@ -36,7 +37,8 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { LegacyMods.Key3, new[] { typeof(ManiaModKey3) } }, new object[] { LegacyMods.Key2, new[] { typeof(ManiaModKey2) } }, new object[] { LegacyMods.Mirror, new[] { typeof(ManiaModMirror) } }, - new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } } + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } }, + new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } }, }; [TestCaseSource(nameof(mania_mod_mapping))] diff --git a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs index 05366e9444..2cf9842c83 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs @@ -4,6 +4,7 @@ using System; using NUnit.Framework; using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Tests.Beatmaps; @@ -28,7 +29,8 @@ namespace osu.Game.Rulesets.Osu.Tests new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } }, new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } }, new object[] { LegacyMods.Target, new[] { typeof(OsuModTargetPractice) } }, - new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } } + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } }, + new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } }, }; [TestCaseSource(nameof(osu_mod_mapping))] diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs index 541987d63e..5f7a78ddf1 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs @@ -4,6 +4,7 @@ using System; using NUnit.Framework; using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Tests.Beatmaps; @@ -25,7 +26,8 @@ namespace osu.Game.Rulesets.Taiko.Tests new object[] { LegacyMods.Flashlight, new[] { typeof(TaikoModFlashlight) } }, new object[] { LegacyMods.Autoplay, new[] { typeof(TaikoModAutoplay) } }, new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } }, - new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } } + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } }, + new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } }, }; [TestCaseSource(nameof(taiko_mod_mapping))] From 7be5e0e97832bfc8fdb4c5989839a820861d8a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 16:15:17 +0200 Subject: [PATCH 0600/2100] Implement back-and-forth conversion of `ModScoreV2` and `LegacyMods` --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 9 +++++++++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 9 +++++++++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 4 ++++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 9 +++++++++ osu.Game/Rulesets/Ruleset.cs | 4 ++++ 5 files changed, 35 insertions(+) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 8f1a1b8ef5..e51e5cc5db 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -91,6 +91,9 @@ namespace osu.Game.Rulesets.Catch if (mods.HasFlagFast(LegacyMods.Relax)) yield return new CatchModRelax(); + + if (mods.HasFlagFast(LegacyMods.ScoreV2)) + yield return new ModScoreV2(); } public override IEnumerable GetModsFor(ModType type) @@ -140,6 +143,12 @@ namespace osu.Game.Rulesets.Catch new CatchModNoScope(), }; + case ModType.System: + return new Mod[] + { + new ModScoreV2(), + }; + default: return Array.Empty(); } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 2e96c89516..bd6ab4086b 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -157,6 +157,9 @@ namespace osu.Game.Rulesets.Mania if (mods.HasFlagFast(LegacyMods.Mirror)) yield return new ManiaModMirror(); + + if (mods.HasFlagFast(LegacyMods.ScoreV2)) + yield return new ModScoreV2(); } public override LegacyMods ConvertToLegacyMods(Mod[] mods) @@ -285,6 +288,12 @@ namespace osu.Game.Rulesets.Mania new ModAdaptiveSpeed() }; + case ModType.System: + return new Mod[] + { + new ModScoreV2(), + }; + default: return Array.Empty(); } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index b44d999d4f..036d13c5aa 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -113,6 +113,9 @@ namespace osu.Game.Rulesets.Osu if (mods.HasFlagFast(LegacyMods.TouchDevice)) yield return new OsuModTouchDevice(); + + if (mods.HasFlagFast(LegacyMods.ScoreV2)) + yield return new ModScoreV2(); } public override LegacyMods ConvertToLegacyMods(Mod[] mods) @@ -212,6 +215,7 @@ namespace osu.Game.Rulesets.Osu return new Mod[] { new OsuModTouchDevice(), + new ModScoreV2(), }; default: diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index aa31b1924f..de3fa1750f 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -116,6 +116,9 @@ namespace osu.Game.Rulesets.Taiko if (mods.HasFlagFast(LegacyMods.Random)) yield return new TaikoModRandom(); + + if (mods.HasFlagFast(LegacyMods.ScoreV2)) + yield return new ModScoreV2(); } public override LegacyMods ConvertToLegacyMods(Mod[] mods) @@ -176,6 +179,12 @@ namespace osu.Game.Rulesets.Taiko new ModAdaptiveSpeed() }; + case ModType.System: + return new Mod[] + { + new ModScoreV2(), + }; + default: return Array.Empty(); } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 490ec1475c..cd432e050b 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -192,6 +192,10 @@ namespace osu.Game.Rulesets case ModAutoplay: value |= LegacyMods.Autoplay; break; + + case ModScoreV2: + value |= LegacyMods.ScoreV2; + break; } } From 9377622cd4010d7141eecaeebd87663fe50f358b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 16:36:51 +0200 Subject: [PATCH 0601/2100] Add backwards migration to populate ScoreV2 mod for already-imported scores --- osu.Game/Database/RealmAccess.cs | 67 +++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index f9f11c49ff..1af0cf30ba 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -15,17 +15,20 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Extensions; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Input.Bindings; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; using osu.Framework.Threading; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Input.Bindings; using osu.Game.IO.Legacy; using osu.Game.Models; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -79,8 +82,9 @@ namespace osu.Game.Database /// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes. /// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations. /// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores. + /// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files. /// - private const int schema_version = 31; + private const int schema_version = 32; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -730,6 +734,8 @@ namespace osu.Game.Database Logger.Log($"Running realm migration to version {targetVersion}..."); Stopwatch stopwatch = new Stopwatch(); + var files = new RealmFileStore(this, storage); + stopwatch.Start(); switch (targetVersion) @@ -904,7 +910,6 @@ namespace osu.Game.Database case 28: { - var files = new RealmFileStore(this, storage); var scores = migration.NewRealm.All(); foreach (var score in scores) @@ -986,6 +991,64 @@ namespace osu.Game.Database break; } + + case 32: + { + foreach (var score in migration.NewRealm.All()) + { + if (!score.IsLegacyScore || !score.Ruleset.IsLegacyRuleset()) + continue; + + string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath(); + if (replayFilename == null) + continue; + + try + { + using (var stream = files.Store.GetStream(replayFilename)) + { + if (stream == null) + continue; + + // Trimmed down logic from LegacyScoreDecoder to extract the mods bitmask. + using (SerializationReader sr = new SerializationReader(stream)) + { + sr.ReadByte(); // Ruleset. + sr.ReadInt32(); // Version. + sr.ReadString(); // Beatmap hash. + sr.ReadString(); // Username. + sr.ReadString(); // MD5Hash. + sr.ReadUInt16(); // Count300. + sr.ReadUInt16(); // Count100. + sr.ReadUInt16(); // Count50. + sr.ReadUInt16(); // CountGeki. + sr.ReadUInt16(); // CountKatu. + sr.ReadUInt16(); // CountMiss. + + // we should have this in LegacyTotalScore already, but if we're reading through this anyways... + int totalScore = sr.ReadInt32(); + + sr.ReadUInt16(); // Max combo. + sr.ReadBoolean(); // Perfect. + + var legacyMods = (LegacyMods)sr.ReadInt32(); + + if (!legacyMods.HasFlagFast(LegacyMods.ScoreV2) || score.APIMods.Any(mod => mod.Acronym == @"SV2")) + continue; + + score.APIMods = score.APIMods.Append(new APIMod(new ModScoreV2())).ToArray(); + score.LegacyTotalScore = score.TotalScore = totalScore; + } + } + } + catch (Exception e) + { + Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database); + } + } + + break; + } } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); From e12255bbe5e41a6cba7acb28282f74c008769575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 17:01:28 +0200 Subject: [PATCH 0602/2100] Do not run legacy conversion with ScoreV2 mod present --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 60530c31cb..bc3629c25b 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -9,6 +9,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -205,6 +206,10 @@ namespace osu.Game.Database if (ruleset is not ILegacyRuleset legacyRuleset) return score.TotalScore; + var mods = score.Mods; + if (mods.Any(mod => mod is ModScoreV2)) + return score.TotalScore; + var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods); if (playableBeatmap.HitObjects.Count == 0) @@ -212,7 +217,7 @@ namespace osu.Game.Database ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator(); - sv1Simulator.Simulate(beatmap, playableBeatmap, score.Mods); + sv1Simulator.Simulate(beatmap, playableBeatmap, mods); return ConvertFromLegacyTotalScore(score, new DifficultyAttributes { From 45194b2b4a4ebb26ce3f1fc2e948f4d21495f828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 18:21:43 +0200 Subject: [PATCH 0603/2100] Fix pressing Ctrl-C in composer not copying timestamp to system clipboard --- .../Screens/Edit/Compose/ComposeScreen.cs | 25 +++++++++------- osu.Game/Screens/Edit/EditorScreen.cs | 29 +++++++------------ 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index dc026f7eac..433fb5c8ee 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -101,26 +101,31 @@ namespace osu.Game.Screens.Edit.Compose #region Clipboard operations - protected override void PerformCut() + public override void Cut() { - base.PerformCut(); + if (!CanCut.Value) + return; Copy(); EditorBeatmap.RemoveRange(EditorBeatmap.SelectedHitObjects.ToArray()); } - protected override void PerformCopy() + public override void Copy() { - base.PerformCopy(); + // on stable, pressing Ctrl-C would copy the current timestamp to system clipboard + // regardless of whether anything was even selected at all. + // UX-wise this is generally strange and unexpected, but make it work anyways to preserve muscle memory. + // note that this means that `getTimestamp()` must handle no-selection case, too. + host.GetClipboard()?.SetText(getTimestamp()); - clipboard.Value = new ClipboardContent(EditorBeatmap).Serialize(); - - host.GetClipboard()?.SetText(formatSelectionAsString()); + if (CanCopy.Value) + clipboard.Value = new ClipboardContent(EditorBeatmap).Serialize(); } - protected override void PerformPaste() + public override void Paste() { - base.PerformPaste(); + if (!CanPaste.Value) + return; var objects = clipboard.Value.Deserialize().HitObjects; @@ -147,7 +152,7 @@ namespace osu.Game.Screens.Edit.Compose CanPaste.Value = composer.IsLoaded && !string.IsNullOrEmpty(clipboard.Value); } - private string formatSelectionAsString() + private string getTimestamp() { if (composer == null) return string.Empty; diff --git a/osu.Game/Screens/Edit/EditorScreen.cs b/osu.Game/Screens/Edit/EditorScreen.cs index b39c0cf5f3..3bc870b898 100644 --- a/osu.Game/Screens/Edit/EditorScreen.cs +++ b/osu.Game/Screens/Edit/EditorScreen.cs @@ -44,29 +44,23 @@ namespace osu.Game.Screens.Edit /// /// Performs a "cut to clipboard" operation appropriate for the given screen. /// - protected virtual void PerformCut() + /// + /// Implementors are responsible for checking themselves. + /// + public virtual void Cut() { } - public void Cut() - { - if (CanCut.Value) - PerformCut(); - } - public BindableBool CanCopy { get; } = new BindableBool(); /// /// Performs a "copy to clipboard" operation appropriate for the given screen. /// - protected virtual void PerformCopy() - { - } - + /// + /// Implementors are responsible for checking themselves. + /// public virtual void Copy() { - if (CanCopy.Value) - PerformCopy(); } public BindableBool CanPaste { get; } = new BindableBool(); @@ -74,14 +68,11 @@ namespace osu.Game.Screens.Edit /// /// Performs a "paste from clipboard" operation appropriate for the given screen. /// - protected virtual void PerformPaste() - { - } - + /// + /// Implementors are responsible for checking themselves. + /// public virtual void Paste() { - if (CanPaste.Value) - PerformPaste(); } #endregion From d135b3f6f57a6d3e67ec45b3a250bbf58451ae4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 21:27:33 +0200 Subject: [PATCH 0604/2100] Add message length limit field to API response --- osu.Game/Online/Chat/Channel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index 761e8aba8d..3f43560f41 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -86,6 +86,9 @@ namespace osu.Game.Online.Chat [JsonProperty(@"last_read_id")] public long? LastReadId; + [JsonProperty(@"message_length_limit")] + public int MessageLengthLimit; + /// /// Signals if the current user joined this channel or not. Defaults to false. /// Note that this does not guarantee a join has completed. Check Id > 0 for confirmation. From 2af8c7bc24e3435dba1c7feb08bd1ee923bc9d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 21:35:24 +0200 Subject: [PATCH 0605/2100] Add failing test case for chat message length limit enforcement --- .../Visual/Online/TestSceneChatTextBox.cs | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs b/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs index 1e80acd56b..8c5475223c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs @@ -3,11 +3,13 @@ #nullable disable +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.Testing; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; @@ -18,7 +20,7 @@ using osu.Game.Overlays.Chat; namespace osu.Game.Tests.Visual.Online { [TestFixture] - public partial class TestSceneChatTextBox : OsuTestScene + public partial class TestSceneChatTextBox : OsuManualInputManagerTestScene { [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); @@ -30,6 +32,8 @@ namespace osu.Game.Tests.Visual.Online private OsuSpriteText searchText; private ChatTextBar bar; + private ChatTextBox textBox => bar.ChildrenOfType().Single(); + [SetUp] public void SetUp() { @@ -115,6 +119,36 @@ namespace osu.Game.Tests.Visual.Online AddStep("Chat Mode Search", () => bar.ShowSearch.Value = true); } + [Test] + public void TestLengthLimit() + { + var firstChannel = new Channel + { + Name = "#test1", + Type = ChannelType.Public, + Id = 4567, + MessageLengthLimit = 20 + }; + var secondChannel = new Channel + { + Name = "#test2", + Type = ChannelType.Public, + Id = 5678, + MessageLengthLimit = 5 + }; + + AddStep("switch to channel with 20 char length limit", () => currentChannel.Value = firstChannel); + AddStep("type a message", () => textBox.Current.Value = "abcdefgh"); + + AddStep("switch to channel with 5 char length limit", () => currentChannel.Value = secondChannel); + AddAssert("text box empty", () => textBox.Current.Value, () => Is.Empty); + AddStep("type too much", () => textBox.Current.Value = "123456"); + AddAssert("text box has 5 chars", () => textBox.Current.Value, () => Has.Length.EqualTo(5)); + + AddStep("switch back to channel with 20 char length limit", () => currentChannel.Value = firstChannel); + AddAssert("unsent message preserved without truncation", () => textBox.Current.Value, () => Is.EqualTo("abcdefgh")); + } + private static Channel createPublicChannel(string name) => new Channel { Name = name, Type = ChannelType.Public, Id = 1234 }; From 6453ab6049937e6e47d114ac5baa979771b369f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 21:42:13 +0200 Subject: [PATCH 0606/2100] Set chat text box message length limit based on channel --- osu.Game/Overlays/Chat/ChatTextBar.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index 87e787fcb8..16a8d14b10 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -156,7 +156,11 @@ namespace osu.Game.Overlays.Chat chatTextBox.Current.UnbindFrom(change.OldValue.TextBoxMessage); if (newChannel != null) + { + // change length limit first before binding to avoid accidentally truncating pending message from new channel. + chatTextBox.LengthLimit = newChannel.MessageLengthLimit; chatTextBox.Current.BindTo(newChannel.TextBoxMessage); + } }, true); } From 91e286560ef53428cc6159e4df6b1c641e503c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 21:49:50 +0200 Subject: [PATCH 0607/2100] Fix search being broken in channel listing "channel" --- osu.Game/Online/Chat/Channel.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index 3f43560f41..15ce926039 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -12,6 +12,7 @@ using osu.Framework.Bindables; using osu.Framework.Lists; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Chat; +using osu.Game.Overlays.Chat.Listing; namespace osu.Game.Online.Chat { @@ -86,8 +87,11 @@ namespace osu.Game.Online.Chat [JsonProperty(@"last_read_id")] public long? LastReadId; + /// + /// Purposefully nullable for the sake of . + /// [JsonProperty(@"message_length_limit")] - public int MessageLengthLimit; + public int? MessageLengthLimit; /// /// Signals if the current user joined this channel or not. Defaults to false. From 89b110e3aa318a16bab399df07408576ac9d9aae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jul 2023 21:26:20 +0900 Subject: [PATCH 0608/2100] Update framework --- 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 270a0aa44b..759167829c 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -11,7 +11,7 @@ manifestmerger.jar - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 184a77a286..7968364243 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 51bcc36621..2e691da079 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -16,6 +16,6 @@ iossimulator-x64 - + From 9a2915f423fa03cbc3daf4a603c0db849f8693b7 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Mon, 10 Jul 2023 17:29:49 +0300 Subject: [PATCH 0609/2100] Add beatmap minimum length checks --- osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 1 + .../Rulesets/Edit/Checks/CheckDrainTime.cs | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckDrainTime.cs diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index dc73e35923..3988f29e13 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Edit new CheckUnsnappedObjects(), new CheckConcurrentObjects(), new CheckZeroLengthObjects(), + new CheckDrainTime(), // Timing new CheckPreviewTime(), diff --git a/osu.Game/Rulesets/Edit/Checks/CheckDrainTime.cs b/osu.Game/Rulesets/Edit/Checks/CheckDrainTime.cs new file mode 100644 index 0000000000..99a74e6479 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckDrainTime.cs @@ -0,0 +1,38 @@ +// 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 osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckDrainTime : ICheck + { + private const int min_drain_threshold = 30 * 1000; + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Compose, "Too short drain time"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateTooShort(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + double drainTime = context.Beatmap.CalculatePlayableLength(); + + if (drainTime < min_drain_threshold) + yield return new IssueTemplateTooShort(this).Create((int)(drainTime / 1000)); + } + + public class IssueTemplateTooShort : IssueTemplate + { + public IssueTemplateTooShort(ICheck check) + : base(check, IssueType.Problem, "Less than 30 seconds of drain time, currently {0}.") + { + } + + public Issue Create(int drainTimeSeconds) => new Issue(this, drainTimeSeconds); + } + } +} From c972a4195c93690d22427303c8840e411455c616 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Mon, 10 Jul 2023 17:29:59 +0300 Subject: [PATCH 0610/2100] Add tests --- .../Editing/Checks/CheckDrainTimeTest.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs new file mode 100644 index 0000000000..ffa3af8fc6 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs @@ -0,0 +1,50 @@ +// 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 NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Editing.Checks +{ + public class CheckDrainTimeTest + { + private CheckDrainTime check = null!; + + private IBeatmap beatmap = null!; + + [SetUp] + public void Setup() + { + check = new CheckDrainTime(); + } + + [Test] + public void TestDrainTimeShort() + { + setShortDrainTimeBeatmap(); + var content = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(content).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckDrainTime.IssueTemplateTooShort); + } + + private void setShortDrainTimeBeatmap() + { + beatmap = new Beatmap + { + HitObjects = + { + new HitCircle() + } + }; + } + } +} From 9e4ffc8c12cdce5bb34fee06fafdec76ac41337e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Jul 2023 21:10:01 +0200 Subject: [PATCH 0611/2100] Split helper method for populations from replay --- osu.Game/Database/RealmAccess.cs | 93 ++++++------------- .../StandardisedScoreMigrationTools.cs | 35 +++++++ 2 files changed, 63 insertions(+), 65 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 1af0cf30ba..f32b161bb6 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -26,7 +26,6 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Input.Bindings; -using osu.Game.IO.Legacy; using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -914,31 +913,13 @@ namespace osu.Game.Database foreach (var score in scores) { - string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath(); - if (replayFilename == null) - continue; - - try + score.PopulateFromReplay(files, sr => { - using (var stream = files.Store.GetStream(replayFilename)) - { - if (stream == null) - continue; - - // Trimmed down logic from LegacyScoreDecoder to extract the version from replays. - using (SerializationReader sr = new SerializationReader(stream)) - { - sr.ReadByte(); // Ruleset. - int version = sr.ReadInt32(); - if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION) - score.IsLegacyScore = true; - } - } - } - catch (Exception e) - { - Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database); - } + sr.ReadByte(); // Ruleset. + int version = sr.ReadInt32(); + if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION) + score.IsLegacyScore = true; + }); } break; @@ -999,52 +980,34 @@ namespace osu.Game.Database if (!score.IsLegacyScore || !score.Ruleset.IsLegacyRuleset()) continue; - string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath(); - if (replayFilename == null) - continue; - - try + score.PopulateFromReplay(files, sr => { - using (var stream = files.Store.GetStream(replayFilename)) - { - if (stream == null) - continue; + sr.ReadByte(); // Ruleset. + sr.ReadInt32(); // Version. + sr.ReadString(); // Beatmap hash. + sr.ReadString(); // Username. + sr.ReadString(); // MD5Hash. + sr.ReadUInt16(); // Count300. + sr.ReadUInt16(); // Count100. + sr.ReadUInt16(); // Count50. + sr.ReadUInt16(); // CountGeki. + sr.ReadUInt16(); // CountKatu. + sr.ReadUInt16(); // CountMiss. - // Trimmed down logic from LegacyScoreDecoder to extract the mods bitmask. - using (SerializationReader sr = new SerializationReader(stream)) - { - sr.ReadByte(); // Ruleset. - sr.ReadInt32(); // Version. - sr.ReadString(); // Beatmap hash. - sr.ReadString(); // Username. - sr.ReadString(); // MD5Hash. - sr.ReadUInt16(); // Count300. - sr.ReadUInt16(); // Count100. - sr.ReadUInt16(); // Count50. - sr.ReadUInt16(); // CountGeki. - sr.ReadUInt16(); // CountKatu. - sr.ReadUInt16(); // CountMiss. + // we should have this in LegacyTotalScore already, but if we're reading through this anyways... + int totalScore = sr.ReadInt32(); - // we should have this in LegacyTotalScore already, but if we're reading through this anyways... - int totalScore = sr.ReadInt32(); + sr.ReadUInt16(); // Max combo. + sr.ReadBoolean(); // Perfect. - sr.ReadUInt16(); // Max combo. - sr.ReadBoolean(); // Perfect. + var legacyMods = (LegacyMods)sr.ReadInt32(); - var legacyMods = (LegacyMods)sr.ReadInt32(); + if (!legacyMods.HasFlagFast(LegacyMods.ScoreV2) || score.APIMods.Any(mod => mod.Acronym == @"SV2")) + return; - if (!legacyMods.HasFlagFast(LegacyMods.ScoreV2) || score.APIMods.Any(mod => mod.Acronym == @"SV2")) - continue; - - score.APIMods = score.APIMods.Append(new APIMod(new ModScoreV2())).ToArray(); - score.LegacyTotalScore = score.TotalScore = totalScore; - } - } - } - catch (Exception e) - { - Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database); - } + score.APIMods = score.APIMods.Append(new APIMod(new ModScoreV2())).ToArray(); + score.LegacyTotalScore = score.TotalScore = totalScore; + }); } break; diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index bc3629c25b..b8afdad294 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -5,7 +5,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using osu.Framework.Logging; using osu.Game.Beatmaps; +using osu.Game.Extensions; +using osu.Game.IO.Legacy; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Judgements; @@ -287,6 +290,38 @@ namespace osu.Game.Database } } + /// + /// Used to populate the model using data parsed from its corresponding replay file. + /// + /// The score to run population from replay for. + /// A instance to use for fetching replay. + /// + /// Delegate describing the population to execute. + /// The delegate's argument is a instance which permits to read data from the replay stream. + /// + public static void PopulateFromReplay(this ScoreInfo score, RealmFileStore files, Action populationFunc) + { + string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath(); + if (replayFilename == null) + return; + + try + { + using (var stream = files.Store.GetStream(replayFilename)) + { + if (stream == null) + return; + + using (SerializationReader sr = new SerializationReader(stream)) + populationFunc.Invoke(sr); + } + } + catch (Exception e) + { + Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database); + } + } + private class FakeHit : HitObject { private readonly Judgement judgement; From 06e5ef88c041c6298e3b2d1e8d64c5f28171d2ac Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 11 Jul 2023 02:30:16 +0200 Subject: [PATCH 0612/2100] legacy export broken --- osu.Game/Beatmaps/BeatmapManager.cs | 53 ++++++++++++++++++++ osu.Game/Beatmaps/BeatmapSetInfo.cs | 13 +++++ osu.Game/Database/ModelManager.cs | 2 + osu.Game/Localisation/EditorStrings.cs | 5 ++ osu.Game/Rulesets/Objects/BezierConverter.cs | 7 +++ osu.Game/Screens/Edit/Editor.cs | 7 +++ 6 files changed, 87 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 305dc01844..7d8c47ea5a 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -18,12 +18,15 @@ using osu.Framework.Platform; using osu.Game.Beatmaps.Formats; using osu.Game.Database; using osu.Game.Extensions; +using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; using osu.Game.Utils; @@ -400,6 +403,56 @@ namespace osu.Game.Beatmaps public Task Export(BeatmapSetInfo beatmap) => beatmapExporter.ExportAsync(beatmap.ToLive(Realm)); + /// + /// Creates a copy of the and converts all beatmaps to legacy format, then exports it as a legacy package. + /// + /// + /// + public Task ExportLegacy(BeatmapSetInfo beatmap) + { + var copy = beatmap.Clone(); // does the detach from realm + + // convert all beatmaps to legacy format + foreach (var beatmapInfo in copy.Beatmaps) + { + // Convert beatmap + var file = beatmapInfo.File; + + if (file == null) + continue; + + using var oldStream = new LineBufferedReader(ReadFile(file)); + var beatmapContent = new LegacyBeatmapDecoder().Decode(oldStream); + + foreach (var controlPoint in beatmapContent.ControlPointInfo.AllControlPoints) + controlPoint.Time = Math.Floor(controlPoint.Time); + + foreach (var hitObject in beatmapContent.HitObjects) + { + hitObject.StartTime = Math.Floor(hitObject.StartTime); + + if (hitObject is IHasPath hasPath && BezierConverter.CountSegments(hasPath.Path.ControlPoints) > 1) + { + var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); + hasPath.Path.ControlPoints.Clear(); + hasPath.Path.ControlPoints.AddRange(newControlPoints); + } + } + + using var newStream = new MemoryStream(); + using var sw = new StreamWriter(newStream, Encoding.UTF8, 1024, true); + new LegacyBeatmapEncoder(beatmapContent, null).Encode(sw); + newStream.Seek(0, SeekOrigin.Begin); + + beatmapInfo.MD5Hash = newStream.ComputeMD5Hash(); + beatmapInfo.Hash = newStream.ComputeSHA2Hash(); + + AddFile(copy, newStream, file.Filename); + } + + return beatmapExporter.ExportAsync(new RealmLiveUnmanaged(copy)); + } + private void updateHashAndMarkDirty(BeatmapSetInfo setInfo) { setInfo.Hash = beatmapImporter.ComputeHash(setInfo); diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 59e413d935..923c498df8 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -90,6 +90,19 @@ namespace osu.Game.Beatmaps return ID == other.ID; } + public BeatmapSetInfo Clone() + { + var clone = (BeatmapSetInfo)this.Detach().MemberwiseClone(); + + for (int i = 0; i < clone.Beatmaps.Count; i++) + clone.Beatmaps[i] = clone.Beatmaps[i].Clone(); + + for (int i = 0; i < clone.Files.Count; i++) + clone.Files[i] = new RealmNamedFileUsage(clone.Files[i].File, clone.Files[i].Filename); + + return clone; + } + public override string ToString() => Metadata.GetDisplayString(); public bool Equals(IBeatmapSetInfo? other) => other is BeatmapSetInfo b && Equals(b); diff --git a/osu.Game/Database/ModelManager.cs b/osu.Game/Database/ModelManager.cs index 7d1dc5239a..8dafe0874c 100644 --- a/osu.Game/Database/ModelManager.cs +++ b/osu.Game/Database/ModelManager.cs @@ -33,6 +33,8 @@ namespace osu.Game.Database Realm = realm; } + public Stream ReadFile(RealmNamedFileUsage file) => realmFileStore.Storage.GetStream(file.File.GetStoragePath()); + public void DeleteFile(TModel item, RealmNamedFileUsage file) => performFileOperation(item, managed => DeleteFile(managed, managed.Files.First(f => f.Filename == file.Filename), managed.Realm)); diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 20258b9c35..f1f650cf66 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -39,6 +39,11 @@ namespace osu.Game.Localisation /// public static LocalisableString ExportPackage => new TranslatableString(getKey(@"export_package"), @"Export package"); + /// + /// "Export legacy package" + /// + public static LocalisableString ExportLegacyPackage => new TranslatableString(getKey(@"export_legacy_package"), @"Export legacy package"); + /// /// "Create new difficulty" /// diff --git a/osu.Game/Rulesets/Objects/BezierConverter.cs b/osu.Game/Rulesets/Objects/BezierConverter.cs index ebee36a7db..0c878fa1fd 100644 --- a/osu.Game/Rulesets/Objects/BezierConverter.cs +++ b/osu.Game/Rulesets/Objects/BezierConverter.cs @@ -39,6 +39,13 @@ namespace osu.Game.Rulesets.Objects new[] { new Vector2d(1, 0), new Vector2d(1, 1.2447058f), new Vector2d(-0.8526471f, 2.118367f), new Vector2d(-2.6211002f, 7.854936e-06f), new Vector2d(-0.8526448f, -2.118357f), new Vector2d(1, -1.2447058f), new Vector2d(1, 0) }) }; + /// + /// Counts the number of segments in a slider path. + /// + /// The control points of the path. + /// The number of segments in a slider path. + public static int CountSegments(IList controlPoints) => controlPoints.Where((t, i) => t.Type != null && i < controlPoints.Count - 1).Count(); + /// /// Converts a slider path to bezier control point positions compatible with the legacy osu! client. /// diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b8fa7f6579..bc5df37570 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -966,6 +966,7 @@ namespace osu.Game.Screens.Edit { new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), new EditorMenuItem(EditorStrings.ExportPackage, MenuItemType.Standard, exportBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + new EditorMenuItem(EditorStrings.ExportLegacyPackage, MenuItemType.Standard, exportLegacyBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, new EditorMenuItemSpacer(), createDifficultyCreationMenu(), createDifficultySwitchMenu(), @@ -981,6 +982,12 @@ namespace osu.Game.Screens.Edit beatmapManager.Export(Beatmap.Value.BeatmapSetInfo); } + private void exportLegacyBeatmap() + { + Save(); + beatmapManager.ExportLegacy(Beatmap.Value.BeatmapSetInfo); + } + /// /// Beatmaps of the currently edited set, grouped by ruleset and ordered by difficulty. /// From 82364b4f9f8ceb5fdc5512fb763067b84cf3696e Mon Sep 17 00:00:00 2001 From: Zyf Date: Tue, 11 Jul 2023 02:46:32 +0200 Subject: [PATCH 0613/2100] Use OsuScoreProcessor in the scoring test scene --- osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs index 2b378c8013..c722d67ac9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs @@ -17,7 +17,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; -using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; @@ -125,8 +125,8 @@ namespace osu.Game.Tests.Visual.Gameplay graphs.Clear(); legend.Clear(); - runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()), ScoringMode.Standardised); - runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()), ScoringMode.Classic); + runForProcessor("lazer-standardised", Color4.YellowGreen, new OsuScoreProcessor(), ScoringMode.Standardised); + runForProcessor("lazer-classic", Color4.MediumPurple, new OsuScoreProcessor(), ScoringMode.Classic); runScoreV1(); runScoreV2(); From ca9c31b492dc097b92c172703bfdaa5a4ce8d4a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jul 2023 17:29:28 +0900 Subject: [PATCH 0614/2100] Add test coverage of slider blueprint end placement failing outside playfield --- .../Editor/TestSceneSliderPlacementBlueprint.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 7542e00a94..1d136fe9cc 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -61,6 +61,21 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointType(0, PathType.Linear); } + [Test] + public void TestPlaceWithMouseMovementOutsidePlayfield() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(1400, 200)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertLength(1200); + assertControlPointCount(2); + assertControlPointType(0, PathType.Linear); + } + [Test] public void TestPlaceNormalControlPoint() { From a0e6748882caf26d24ec526ec882a47968b7505a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jul 2023 17:29:53 +0900 Subject: [PATCH 0615/2100] Fix slider blueprint placement when ending placement outside the playfield As mentioned in https://github.com/ppy/osu/discussions/24161 --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 717c026ded..5cb9adfd72 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Edit /// protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent?.ReceivePositionalInputAt(screenSpacePos) == true; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; protected override bool Handle(UIEvent e) { From 2dcd79044289fe31629965ffcbb6fd11e678790e Mon Sep 17 00:00:00 2001 From: Susko3 Date: Tue, 11 Jul 2023 11:42:31 +0200 Subject: [PATCH 0616/2100] Resolve `Clipboard` via DI --- osu.Game/Graphics/ScreenshotManager.cs | 5 ++++- osu.Game/Graphics/UserInterface/ExternalLinkButton.cs | 10 ++++++++-- osu.Game/Online/Chat/ExternalLinkOpener.cs | 5 ++++- osu.Game/Overlays/Comments/DrawableComment.cs | 4 ++-- osu.Game/Screens/Edit/Compose/ComposeScreen.cs | 4 ++-- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index 82f89d6889..26e499ae9a 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -43,6 +43,9 @@ namespace osu.Game.Graphics [Resolved] private GameHost host { get; set; } + [Resolved] + private Clipboard clipboard { get; set; } + private Storage storage; [Resolved] @@ -116,7 +119,7 @@ namespace osu.Game.Graphics using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false)) { - host.GetClipboard()?.SetImage(image); + clipboard.SetImage(image); (string filename, var stream) = getWritableStream(); diff --git a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs index 4eccb37613..7ba3d55162 100644 --- a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs +++ b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs @@ -27,6 +27,9 @@ namespace osu.Game.Graphics.UserInterface [Resolved] private GameHost host { get; set; } = null!; + [Resolved] + private Clipboard clipboard { get; set; } = null!; + [Resolved] private OnScreenDisplay? onScreenDisplay { get; set; } @@ -92,8 +95,11 @@ namespace osu.Game.Graphics.UserInterface private void copyUrl() { - host.GetClipboard()?.SetText(Link); - onScreenDisplay?.Display(new CopyUrlToast()); + if (Link != null) + { + clipboard.SetText(Link); + onScreenDisplay?.Display(new CopyUrlToast()); + } } } } diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 201212c648..56d24e35bb 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -18,6 +18,9 @@ namespace osu.Game.Online.Chat [Resolved] private GameHost host { get; set; } = null!; + [Resolved] + private Clipboard clipboard { get; set; } = null!; + [Resolved(CanBeNull = true)] private IDialogOverlay? dialogOverlay { get; set; } @@ -32,7 +35,7 @@ namespace osu.Game.Online.Chat public void OpenUrlExternally(string url, bool bypassWarning = false) { if (!bypassWarning && externalLinkWarning.Value && dialogOverlay != null) - dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => host.GetClipboard()?.SetText(url))); + dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => clipboard.SetText(url))); else host.OpenUrlExternally(url); } diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index a710406548..ba1c7ca8b2 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Comments private IAPIProvider api { get; set; } = null!; [Resolved] - private GameHost host { get; set; } = null!; + private Clipboard clipboard { get; set; } = null!; [Resolved] private OnScreenDisplay? onScreenDisplay { get; set; } @@ -444,7 +444,7 @@ namespace osu.Game.Overlays.Comments private void copyUrl() { - host.GetClipboard()?.SetText($@"{api.APIEndpointUrl}/comments/{Comment.Id}"); + clipboard.SetText($@"{api.APIEndpointUrl}/comments/{Comment.Id}"); onScreenDisplay?.Display(new CopyUrlToast()); } diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 433fb5c8ee..0a58b34da6 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Edit.Compose public partial class ComposeScreen : EditorScreenWithTimeline, IGameplaySettings { [Resolved] - private GameHost host { get; set; } + private Clipboard hostClipboard { get; set; } = null!; [Resolved] private EditorClock clock { get; set; } @@ -116,7 +116,7 @@ namespace osu.Game.Screens.Edit.Compose // regardless of whether anything was even selected at all. // UX-wise this is generally strange and unexpected, but make it work anyways to preserve muscle memory. // note that this means that `getTimestamp()` must handle no-selection case, too. - host.GetClipboard()?.SetText(getTimestamp()); + hostClipboard.SetText(getTimestamp()); if (CanCopy.Value) clipboard.Value = new ClipboardContent(EditorBeatmap).Serialize(); From 1dae1d8f0afa6bb3bae418d5286819bebe4f1612 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Tue, 11 Jul 2023 13:40:19 +0300 Subject: [PATCH 0617/2100] Account for break time --- osu.Game/Rulesets/Edit/Checks/CheckDrainTime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckDrainTime.cs b/osu.Game/Rulesets/Edit/Checks/CheckDrainTime.cs index 99a74e6479..21f053f2c2 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckDrainTime.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckDrainTime.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Edit.Checks public IEnumerable Run(BeatmapVerifierContext context) { - double drainTime = context.Beatmap.CalculatePlayableLength(); + double drainTime = context.Beatmap.CalculatePlayableLength() - context.Beatmap.TotalBreakTime; if (drainTime < min_drain_threshold) yield return new IssueTemplateTooShort(this).Create((int)(drainTime / 1000)); From d75887bb3b54b0e024eb5fe47368b79f169c7733 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Tue, 11 Jul 2023 13:40:27 +0300 Subject: [PATCH 0618/2100] Apply feedback to tests --- .../Editing/Checks/CheckDrainTimeTest.cs | 78 +++++++++++++++---- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs index ffa3af8fc6..9f93ec17d5 100644 --- a/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.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 NUnit.Framework; using osu.Game.Beatmaps; @@ -16,8 +17,6 @@ namespace osu.Game.Tests.Editing.Checks { private CheckDrainTime check = null!; - private IBeatmap beatmap = null!; - [SetUp] public void Setup() { @@ -27,24 +26,77 @@ namespace osu.Game.Tests.Editing.Checks [Test] public void TestDrainTimeShort() { - setShortDrainTimeBeatmap(); - var content = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + assertShortDrainTime(getShortDrainTimeBeatmap()); + } - var issues = check.Run(content).ToList(); + [Test] + public void TestDrainTimeBreak() + { + assertShortDrainTime(getLongBreakBeatmap()); + } + + [Test] + public void TestDrainTimeCorrect() + { + assertOk(getCorrectDrainTimeBeatmap()); + } + + private IBeatmap getShortDrainTimeBeatmap() + { + return new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 } + } + }; + } + + private IBeatmap getLongBreakBeatmap() + { + return new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 30 } + } + }; + } + + private IBeatmap getCorrectDrainTimeBeatmap() + { + var hitObjects = new List(); + + for (int i = 0; i <= 30; ++i) + { + hitObjects.Add(new HitCircle { StartTime = 1000 * i }); + } + + return new Beatmap + { + HitObjects = hitObjects + }; + } + + private void assertShortDrainTime(IBeatmap beatmap) + { + var issues = check.Run(getContext(beatmap)).ToList(); Assert.That(issues, Has.Count.EqualTo(1)); Assert.That(issues.Single().Template is CheckDrainTime.IssueTemplateTooShort); } - private void setShortDrainTimeBeatmap() + private void assertOk(IBeatmap beatmap) { - beatmap = new Beatmap - { - HitObjects = - { - new HitCircle() - } - }; + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Is.Empty); + } + + private BeatmapVerifierContext getContext(IBeatmap beatmap) + { + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); } } } From 2db25722cbf29eb7d93e59df5d3e6b414e4d2dd4 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 11 Jul 2023 20:18:54 +0200 Subject: [PATCH 0619/2100] It works now --- osu.Game/Beatmaps/BeatmapManager.cs | 89 +++++++++++++++++++---------- osu.Game/Beatmaps/BeatmapSetInfo.cs | 13 ----- osu.Game/Database/ModelManager.cs | 10 ++-- 3 files changed, 62 insertions(+), 50 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 7d8c47ea5a..7c21b87a97 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -29,6 +29,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; using osu.Game.Utils; +using osuTK; namespace osu.Game.Beatmaps { @@ -406,51 +407,77 @@ namespace osu.Game.Beatmaps /// /// Creates a copy of the and converts all beatmaps to legacy format, then exports it as a legacy package. /// - /// + /// /// - public Task ExportLegacy(BeatmapSetInfo beatmap) + public Task ExportLegacy(BeatmapSetInfo beatmapSetInfo) { - var copy = beatmap.Clone(); // does the detach from realm + // Create a clone of the original beatmap set which we will convert to legacy format and then export + var clone = new BeatmapSetInfo(beatmapSetInfo.Beatmaps.Select(b => b.Clone())); + clone.Files.AddRange(beatmapSetInfo.Files.Select(f => new RealmNamedFileUsage(f.File, f.Filename))); - // convert all beatmaps to legacy format - foreach (var beatmapInfo in copy.Beatmaps) + // convert all beatmaps in the cloned beatmap set to legacy format + foreach (var beatmapInfo in clone.Beatmaps) { - // Convert beatmap - var file = beatmapInfo.File; + beatmapInfo.BeatmapSet = clone; + beatmapInfo.ID = Guid.NewGuid(); + var file = beatmapInfo.File; if (file == null) continue; - using var oldStream = new LineBufferedReader(ReadFile(file)); - var beatmapContent = new LegacyBeatmapDecoder().Decode(oldStream); - - foreach (var controlPoint in beatmapContent.ControlPointInfo.AllControlPoints) - controlPoint.Time = Math.Floor(controlPoint.Time); - - foreach (var hitObject in beatmapContent.HitObjects) + var beatmapContent = new LegacyBeatmapDecoder().Decode(new LineBufferedReader(RealmFileStore.Storage.GetStream(file.File.GetStoragePath()))); + var beatmapSkin = new LegacySkin(new SkinInfo(), null!) { - hitObject.StartTime = Math.Floor(hitObject.StartTime); + Configuration = new LegacySkinDecoder().Decode(new LineBufferedReader(RealmFileStore.Storage.GetStream(file.File.GetStoragePath()))) + }; - if (hitObject is IHasPath hasPath && BezierConverter.CountSegments(hasPath.Path.ControlPoints) > 1) - { - var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); - hasPath.Path.ControlPoints.Clear(); - hasPath.Path.ControlPoints.AddRange(newControlPoints); - } - } + Realm.Realm.Write(realm => + { + using var stream = new MemoryStream(); + convertAndEncodeLegacyBeatmap(beatmapContent, beatmapSkin, stream); - using var newStream = new MemoryStream(); - using var sw = new StreamWriter(newStream, Encoding.UTF8, 1024, true); - new LegacyBeatmapEncoder(beatmapContent, null).Encode(sw); - newStream.Seek(0, SeekOrigin.Begin); + beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); + beatmapInfo.Hash = stream.ComputeSHA2Hash(); - beatmapInfo.MD5Hash = newStream.ComputeMD5Hash(); - beatmapInfo.Hash = newStream.ComputeSHA2Hash(); - - AddFile(copy, newStream, file.Filename); + file.File = RealmFileStore.Add(stream, realm).Detach(); + }); } - return beatmapExporter.ExportAsync(new RealmLiveUnmanaged(copy)); + return beatmapExporter.ExportAsync(new RealmLiveUnmanaged(clone)); + } + + private void convertAndEncodeLegacyBeatmap(IBeatmap beatmapContent, ISkin beatmapSkin, Stream stream) + { + // Convert beatmap elements to be compatible with legacy format + // So we truncate time and position values to integers, and convert paths with multiple segments to bezier curves + foreach (var controlPoint in beatmapContent.ControlPointInfo.AllControlPoints) + controlPoint.Time = Math.Floor(controlPoint.Time); + + foreach (var hitObject in beatmapContent.HitObjects) + { + hitObject.StartTime = Math.Floor(hitObject.StartTime); + + if (hitObject is not IHasPath hasPath || BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue; + + var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); + + // Truncate control points to integer positions + foreach (var pathControlPoint in newControlPoints) + { + pathControlPoint.Position = new Vector2( + (float)Math.Floor(pathControlPoint.Position.X), + (float)Math.Floor(pathControlPoint.Position.Y)); + } + + hasPath.Path.ControlPoints.Clear(); + hasPath.Path.ControlPoints.AddRange(newControlPoints); + } + + // Encode to legacy format + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); + + stream.Seek(0, SeekOrigin.Begin); } private void updateHashAndMarkDirty(BeatmapSetInfo setInfo) diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 923c498df8..59e413d935 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -90,19 +90,6 @@ namespace osu.Game.Beatmaps return ID == other.ID; } - public BeatmapSetInfo Clone() - { - var clone = (BeatmapSetInfo)this.Detach().MemberwiseClone(); - - for (int i = 0; i < clone.Beatmaps.Count; i++) - clone.Beatmaps[i] = clone.Beatmaps[i].Clone(); - - for (int i = 0; i < clone.Files.Count; i++) - clone.Files[i] = new RealmNamedFileUsage(clone.Files[i].File, clone.Files[i].Filename); - - return clone; - } - public override string ToString() => Metadata.GetDisplayString(); public bool Equals(IBeatmapSetInfo? other) => other is BeatmapSetInfo b && Equals(b); diff --git a/osu.Game/Database/ModelManager.cs b/osu.Game/Database/ModelManager.cs index 8dafe0874c..172a2df5a3 100644 --- a/osu.Game/Database/ModelManager.cs +++ b/osu.Game/Database/ModelManager.cs @@ -25,16 +25,14 @@ namespace osu.Game.Database protected RealmAccess Realm { get; } - private readonly RealmFileStore realmFileStore; + protected readonly RealmFileStore RealmFileStore; public ModelManager(Storage storage, RealmAccess realm) { - realmFileStore = new RealmFileStore(realm, storage); + RealmFileStore = new RealmFileStore(realm, storage); Realm = realm; } - public Stream ReadFile(RealmNamedFileUsage file) => realmFileStore.Storage.GetStream(file.File.GetStoragePath()); - public void DeleteFile(TModel item, RealmNamedFileUsage file) => performFileOperation(item, managed => DeleteFile(managed, managed.Files.First(f => f.Filename == file.Filename), managed.Realm)); @@ -79,7 +77,7 @@ namespace osu.Game.Database /// public void ReplaceFile(RealmNamedFileUsage file, Stream contents, Realm realm) { - file.File = realmFileStore.Add(contents, realm); + file.File = RealmFileStore.Add(contents, realm); } /// @@ -95,7 +93,7 @@ namespace osu.Game.Database return; } - var file = realmFileStore.Add(contents, realm); + var file = RealmFileStore.Add(contents, realm); var namedUsage = new RealmNamedFileUsage(file, filename); item.Files.Add(namedUsage); From b577b6b6ae9ce8e4b047cfed8ba0820c04546db7 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 11 Jul 2023 21:04:09 +0200 Subject: [PATCH 0620/2100] Export legacy converted beatmaps as .osz and non-converted beatmaps as .osz2 --- osu.Game/Beatmaps/BeatmapImporter.cs | 4 ++-- osu.Game/Beatmaps/BeatmapManager.cs | 13 ++++++++++--- osu.Game/Database/BeatmapExporter.cs | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Database/BeatmapExporter.cs diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 7d367ef77d..a2d74d089f 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps /// public class BeatmapImporter : RealmArchiveModelImporter { - public override IEnumerable HandledExtensions => new[] { ".osz" }; + public override IEnumerable HandledExtensions => new[] { ".osz", ".osz2" }; protected override string[] HashableFileTypes => new[] { ".osu" }; @@ -145,7 +145,7 @@ namespace osu.Game.Beatmaps } } - protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz"; + protected override bool ShouldDeleteArchive(string path) => HandledExtensions.Contains(Path.GetExtension(path).ToLowerInvariant()); protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) { diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 7c21b87a97..ded26f79cd 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -44,7 +44,9 @@ namespace osu.Game.Beatmaps private readonly WorkingBeatmapCache workingBeatmapCache; - private readonly LegacyBeatmapExporter beatmapExporter; + private readonly BeatmapExporter beatmapExporter; + + private readonly LegacyBeatmapExporter legacyBeatmapExporter; public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; } @@ -81,7 +83,12 @@ namespace osu.Game.Beatmaps workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host); - beatmapExporter = new LegacyBeatmapExporter(storage) + beatmapExporter = new BeatmapExporter(storage) + { + PostNotification = obj => PostNotification?.Invoke(obj) + }; + + legacyBeatmapExporter = new LegacyBeatmapExporter(storage) { PostNotification = obj => PostNotification?.Invoke(obj) }; @@ -443,7 +450,7 @@ namespace osu.Game.Beatmaps }); } - return beatmapExporter.ExportAsync(new RealmLiveUnmanaged(clone)); + return legacyBeatmapExporter.ExportAsync(new RealmLiveUnmanaged(clone)); } private void convertAndEncodeLegacyBeatmap(IBeatmap beatmapContent, ISkin beatmapSkin, Stream stream) diff --git a/osu.Game/Database/BeatmapExporter.cs b/osu.Game/Database/BeatmapExporter.cs new file mode 100644 index 0000000000..b3a85dd5d9 --- /dev/null +++ b/osu.Game/Database/BeatmapExporter.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Platform; +using osu.Game.Beatmaps; + +namespace osu.Game.Database +{ + public class BeatmapExporter : LegacyArchiveExporter + { + public BeatmapExporter(Storage storage) + : base(storage) + { + } + + protected override string FileExtension => @".osz2"; + } +} From 9a3cb624a8a616b37df9f35280d659c704f6ae18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Jul 2023 23:18:49 +0200 Subject: [PATCH 0621/2100] Rewrite tests to be less aggressively DRY Four of six helper methods defined in the test were used exactly once; the remaining two were used two times. Splitting helpers there is just too much. --- .../Editing/Checks/CheckDrainTimeTest.cs | 69 +++++++------------ 1 file changed, 23 insertions(+), 46 deletions(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs index 9f93ec17d5..e5a33d753c 100644 --- a/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs @@ -26,35 +26,25 @@ namespace osu.Game.Tests.Editing.Checks [Test] public void TestDrainTimeShort() { - assertShortDrainTime(getShortDrainTimeBeatmap()); - } - - [Test] - public void TestDrainTimeBreak() - { - assertShortDrainTime(getLongBreakBeatmap()); - } - - [Test] - public void TestDrainTimeCorrect() - { - assertOk(getCorrectDrainTimeBeatmap()); - } - - private IBeatmap getShortDrainTimeBeatmap() - { - return new Beatmap + var beatmap = new Beatmap { HitObjects = { new HitCircle { StartTime = 0 } } }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckDrainTime.IssueTemplateTooShort); } - private IBeatmap getLongBreakBeatmap() + [Test] + public void TestDrainTimeBreak() { - return new Beatmap + var beatmap = new Beatmap { HitObjects = { @@ -62,41 +52,28 @@ namespace osu.Game.Tests.Editing.Checks new HitCircle { StartTime = 30 } } }; - } + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); - private IBeatmap getCorrectDrainTimeBeatmap() - { - var hitObjects = new List(); - - for (int i = 0; i <= 30; ++i) - { - hitObjects.Add(new HitCircle { StartTime = 1000 * i }); - } - - return new Beatmap - { - HitObjects = hitObjects - }; - } - - private void assertShortDrainTime(IBeatmap beatmap) - { - var issues = check.Run(getContext(beatmap)).ToList(); + var issues = check.Run(context).ToList(); Assert.That(issues, Has.Count.EqualTo(1)); Assert.That(issues.Single().Template is CheckDrainTime.IssueTemplateTooShort); } - private void assertOk(IBeatmap beatmap) + [Test] + public void TestDrainTimeCorrect() { - var issues = check.Run(getContext(beatmap)).ToList(); + var hitObjects = new List(); + + for (int i = 0; i <= 30; ++i) + hitObjects.Add(new HitCircle { StartTime = 1000 * i }); + + var beatmap = new Beatmap { HitObjects = hitObjects }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); Assert.That(issues, Is.Empty); } - - private BeatmapVerifierContext getContext(IBeatmap beatmap) - { - return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); - } } } From d927cb3f1c3ec6c61e9ab1ee8beced9ecad5938a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Jul 2023 23:21:03 +0200 Subject: [PATCH 0622/2100] Actually cover cases with breaks in tests --- osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs index e5a33d753c..9e20164972 100644 --- a/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Checks; using osu.Game.Rulesets.Objects; @@ -49,7 +50,11 @@ namespace osu.Game.Tests.Editing.Checks HitObjects = { new HitCircle { StartTime = 0 }, - new HitCircle { StartTime = 30 } + new HitCircle { StartTime = 40_000 } + }, + Breaks = new List + { + new BreakPeriod(10_000, 21_000) } }; var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); From a1da0b58db3ebc7ec425df2ff3ae81fc2d9949b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Jul 2023 23:22:37 +0200 Subject: [PATCH 0623/2100] Improve negative test case without breaks too --- osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs index 9e20164972..f845d3c4c1 100644 --- a/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs @@ -31,7 +31,8 @@ namespace osu.Game.Tests.Editing.Checks { HitObjects = { - new HitCircle { StartTime = 0 } + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 29_999 } } }; var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); From 41d39243264fbd1c1d135cf48d430b757c06b0df Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Jul 2023 16:30:26 +0900 Subject: [PATCH 0624/2100] Update localisation analyser packages --- .config/dotnet-tools.json | 2 +- osu.Game/osu.Game.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 8c8a3be771..3cecb0d07c 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -21,7 +21,7 @@ ] }, "ppy.localisationanalyser.tools": { - "version": "2022.809.0", + "version": "2023.712.0", "commands": [ "localisation" ] diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7968364243..8febabb61b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -31,7 +31,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 87570ed238bb303237d40aa3a51a3af46317e17d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jul 2023 17:23:31 +0900 Subject: [PATCH 0625/2100] Fix incorrect slider stacking on very old beatmaps Closes https://github.com/ppy/osu/issues/24185 The stable code has had a bug in this logic forever. So we'll need to reimplement the bug. Basically, sliders have to have `UpdateCalculations` run in order to have a correct `Position2` and `EndTime`, but this wasn't being called in the inner loop before use of `EndTime` at https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/GameplayElements/HitObjectManager.cs#L1813. To fix this, we use `StartTime` in the inner loop to reproduce the bug. --- osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs index f51f04bf87..c081df3ac6 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs @@ -214,17 +214,24 @@ namespace osu.Game.Rulesets.Osu.Beatmaps ? currSlider.Position + currSlider.Path.PositionAt(1) : currHitObject.Position; + // Note the use of `StartTime` in the code below doesn't match stable's use of `EndTime`. + // This is because in the stable implementation, `UpdateCalculations` is not called on the inner-loop hitobject (j) + // and therefore it does not have a correct `EndTime`, but instead the default of `EndTime = StartTime`. + // + // Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where + // if we use `EndTime` here it would result in unexpected stacking. + if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.Position) < stack_distance) { currHitObject.StackHeight++; - startTime = beatmap.HitObjects[j].GetEndTime(); + startTime = beatmap.HitObjects[j].StartTime; } else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, position2) < stack_distance) { // Case for sliders - bump notes down and right, rather than up and left. sliderStack++; beatmap.HitObjects[j].StackHeight -= sliderStack; - startTime = beatmap.HitObjects[j].GetEndTime(); + startTime = beatmap.HitObjects[j].StartTime; } } } From d12845d7b1ecb007b6d2c9602e1042d192842b12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jul 2023 17:39:54 +0900 Subject: [PATCH 0626/2100] Remove no-longer-necessary `ReceivePositionalInputAt` overide in `CatchPlacementBlueprint` --- .../Edit/Blueprints/CatchPlacementBlueprint.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs index d2d605a6fe..1a2990e4ac 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs @@ -6,7 +6,6 @@ using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; -using osuTK; namespace osu.Game.Rulesets.Catch.Edit.Blueprints { @@ -24,7 +23,5 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints : base(new THitObject()) { } - - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; } } From 547f2476694ace63aa44d8f55324e61804030f2d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jul 2023 17:41:58 +0900 Subject: [PATCH 0627/2100] Fix test to work regardless of screen sizes --- .../Editor/TestSceneSliderPlacementBlueprint.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 1d136fe9cc..7d29670daa 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -67,11 +67,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addMovementStep(new Vector2(200)); addClickStep(MouseButton.Left); - addMovementStep(new Vector2(1400, 200)); + AddStep("move mouse out of screen", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + Vector2.One)); addClickStep(MouseButton.Right); assertPlaced(true); - assertLength(1200); assertControlPointCount(2); assertControlPointType(0, PathType.Linear); } From b3b6df6e3034051d87bb2dc9ddc3f74bcce5fab2 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Wed, 12 Jul 2023 06:19:48 -0400 Subject: [PATCH 0628/2100] Remove emoji regex --- osu.Game/Online/Chat/MessageFormatter.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index f89939d7cf..792780595f 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -283,8 +283,6 @@ namespace osu.Game.Online.Chat while (space-- > 0) empty += "\0"; - handleMatches(emoji_regex, empty, "{0}", result, startIndex); - return result; } From 4f4c481a678b8fb5e608ca9c7af5ce4372ac80f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jul 2023 19:21:24 +0900 Subject: [PATCH 0629/2100] Fix timing distribution graph sometimes not displaying correctly Weird "basal" height logic just didn't make any sense (was getting stuck at 1 when `DrawHeight` was 0) --- .../HitEventTimingDistributionGraph.cs | 92 ++++++------------- 1 file changed, 27 insertions(+), 65 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 6b1850002d..16da8c64a0 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -211,7 +211,8 @@ namespace osu.Game.Screens.Ranking.Statistics private readonly bool isCentre; private readonly float totalValue; - private float basalHeight; + private const float minimum_height = 0.02f; + private float offsetAdjustment; private Circle[] boxOriginals = null!; @@ -256,15 +257,17 @@ namespace osu.Game.Screens.Ranking.Statistics else { // A bin with no value draws a grey dot instead. - Circle dot = new Circle + InternalChildren = boxOriginals = new[] { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Colour = isCentre ? Color4.White : Color4.Gray, - Height = 0, + new Circle + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Colour = isCentre ? Color4.White : Color4.Gray, + Height = 0, + } }; - InternalChildren = boxOriginals = new[] { dot }; } } @@ -272,31 +275,24 @@ namespace osu.Game.Screens.Ranking.Statistics { base.LoadComplete(); - if (!values.Any()) - return; - - updateBasalHeight(); - - foreach (var boxOriginal in boxOriginals) - { - boxOriginal.Y = 0; - boxOriginal.Height = basalHeight; - } - float offsetValue = 0; - for (int i = 0; i < values.Count; i++) + for (int i = 0; i < boxOriginals.Length; i++) { - boxOriginals[i].MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint); - boxOriginals[i].ResizeHeightTo(heightForValue(values[i].Value), duration, Easing.OutQuint); - offsetValue -= values[i].Value; - } - } + int value = i < values.Count ? values[i].Value : 0; - protected override void Update() - { - base.Update(); - updateBasalHeight(); + var box = boxOriginals[i]; + + box.Y = 0; + box.Height = 0; + + box.MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint); + box.ResizeHeightTo(heightForValue(value), duration, Easing.OutQuint); + offsetValue -= value; + } + + if (boxAdjustment != null) + drawAdjustmentBar(); } public void UpdateOffset(float adjustment) @@ -324,43 +320,9 @@ namespace osu.Game.Screens.Ranking.Statistics drawAdjustmentBar(); } - private void updateBasalHeight() - { - float newBasalHeight = DrawHeight > DrawWidth ? DrawWidth / DrawHeight : 1; + private float offsetForValue(float value) => (1 - minimum_height) * value / maxValue; - if (newBasalHeight == basalHeight) - return; - - basalHeight = newBasalHeight; - foreach (var dot in boxOriginals) - dot.Height = basalHeight; - - draw(); - } - - private float offsetForValue(float value) => (1 - basalHeight) * value / maxValue; - - private float heightForValue(float value) => MathF.Max(basalHeight + offsetForValue(value), 0); - - private void draw() - { - resizeBars(); - - if (boxAdjustment != null) - drawAdjustmentBar(); - } - - private void resizeBars() - { - float offsetValue = 0; - - for (int i = 0; i < values.Count; i++) - { - boxOriginals[i].Y = offsetForValue(offsetValue) * DrawHeight; - boxOriginals[i].Height = heightForValue(values[i].Value); - offsetValue -= values[i].Value; - } - } + private float heightForValue(float value) => minimum_height + offsetForValue(value); private void drawAdjustmentBar() { From b05ba8c501cd1d43b19bbe057ca939fdca6e4f06 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Wed, 12 Jul 2023 06:32:33 -0400 Subject: [PATCH 0630/2100] Remove unused code --- osu.Game/Online/Chat/MessageFormatter.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 792780595f..6ca651bc87 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -279,10 +279,6 @@ namespace osu.Game.Online.Chat // handle channels handleMatches(channel_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}chan/{{0}}", result, startIndex, LinkAction.OpenChannel); - string empty = ""; - while (space-- > 0) - empty += "\0"; - return result; } From 465cc759f02c84c6c3ae720d715623ac2b7596d6 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 12 Jul 2023 14:49:49 +0200 Subject: [PATCH 0631/2100] Add xmldoc to clarify the purpose of BeatmapExporter --- osu.Game/Database/BeatmapExporter.cs | 4 ++++ osu.Game/Database/LegacyBeatmapExporter.cs | 3 +++ 2 files changed, 7 insertions(+) diff --git a/osu.Game/Database/BeatmapExporter.cs b/osu.Game/Database/BeatmapExporter.cs index b3a85dd5d9..9377065973 100644 --- a/osu.Game/Database/BeatmapExporter.cs +++ b/osu.Game/Database/BeatmapExporter.cs @@ -6,6 +6,10 @@ using osu.Game.Beatmaps; namespace osu.Game.Database { + /// + /// Exporter for beatmap archives. + /// This is not for legacy purposes and works for lazer only. + /// public class BeatmapExporter : LegacyArchiveExporter { public BeatmapExporter(Storage storage) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 4ee8c0636e..62096f862a 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -6,6 +6,9 @@ using osu.Game.Beatmaps; namespace osu.Game.Database { + /// + /// Exporter for osu!stable legacy beatmap archives. + /// public class LegacyBeatmapExporter : LegacyArchiveExporter { public LegacyBeatmapExporter(Storage storage) From 3052c317e10c801e79a45542bcc247ee59379ce5 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 12 Jul 2023 15:04:06 +0200 Subject: [PATCH 0632/2100] change .osz2 to .olz (osu lazer zip) --- osu.Game/Beatmaps/BeatmapImporter.cs | 2 +- osu.Game/Database/BeatmapExporter.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index a2d74d089f..e500f87984 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps /// public class BeatmapImporter : RealmArchiveModelImporter { - public override IEnumerable HandledExtensions => new[] { ".osz", ".osz2" }; + public override IEnumerable HandledExtensions => new[] { ".osz", ".olz" }; protected override string[] HashableFileTypes => new[] { ".osu" }; diff --git a/osu.Game/Database/BeatmapExporter.cs b/osu.Game/Database/BeatmapExporter.cs index 9377065973..f37c57dea5 100644 --- a/osu.Game/Database/BeatmapExporter.cs +++ b/osu.Game/Database/BeatmapExporter.cs @@ -17,6 +17,6 @@ namespace osu.Game.Database { } - protected override string FileExtension => @".osz2"; + protected override string FileExtension => @".olz"; } } From 8ca801a2240b6749c35dc98a33f73ba229d080fe Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 12 Jul 2023 15:18:16 +0200 Subject: [PATCH 0633/2100] dispose the streams --- osu.Game/Beatmaps/BeatmapManager.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index ded26f79cd..248e97d6ee 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -432,10 +432,15 @@ namespace osu.Game.Beatmaps if (file == null) continue; - var beatmapContent = new LegacyBeatmapDecoder().Decode(new LineBufferedReader(RealmFileStore.Storage.GetStream(file.File.GetStoragePath()))); + using var contentStream = RealmFileStore.Storage.GetStream(file.File.GetStoragePath()); + using var contentStreamReader = new LineBufferedReader(contentStream); + var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader); + + using var skinStream = RealmFileStore.Storage.GetStream(file.File.GetStoragePath()); + using var skinStreamReader = new LineBufferedReader(contentStream); var beatmapSkin = new LegacySkin(new SkinInfo(), null!) { - Configuration = new LegacySkinDecoder().Decode(new LineBufferedReader(RealmFileStore.Storage.GetStream(file.File.GetStoragePath()))) + Configuration = new LegacySkinDecoder().Decode(skinStreamReader) }; Realm.Realm.Write(realm => From 1d837a8725308a7f6e83ced3713a8796ad464930 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 13 Jul 2023 00:20:01 +0200 Subject: [PATCH 0634/2100] Move all conversion code to LegacyBeatmapExporter --- osu.Game/Beatmaps/BeatmapManager.cs | 85 +--------------------- osu.Game/Database/LegacyArchiveExporter.cs | 4 +- osu.Game/Database/LegacyBeatmapExporter.cs | 70 ++++++++++++++++++ osu.Game/Database/ModelManager.cs | 8 +- 4 files changed, 78 insertions(+), 89 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 248e97d6ee..ffdff13845 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -18,18 +18,14 @@ using osu.Framework.Platform; using osu.Game.Beatmaps.Formats; using osu.Game.Database; using osu.Game.Extensions; -using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; using osu.Game.Utils; -using osuTK; namespace osu.Game.Beatmaps { @@ -411,86 +407,7 @@ namespace osu.Game.Beatmaps public Task Export(BeatmapSetInfo beatmap) => beatmapExporter.ExportAsync(beatmap.ToLive(Realm)); - /// - /// Creates a copy of the and converts all beatmaps to legacy format, then exports it as a legacy package. - /// - /// - /// - public Task ExportLegacy(BeatmapSetInfo beatmapSetInfo) - { - // Create a clone of the original beatmap set which we will convert to legacy format and then export - var clone = new BeatmapSetInfo(beatmapSetInfo.Beatmaps.Select(b => b.Clone())); - clone.Files.AddRange(beatmapSetInfo.Files.Select(f => new RealmNamedFileUsage(f.File, f.Filename))); - - // convert all beatmaps in the cloned beatmap set to legacy format - foreach (var beatmapInfo in clone.Beatmaps) - { - beatmapInfo.BeatmapSet = clone; - beatmapInfo.ID = Guid.NewGuid(); - - var file = beatmapInfo.File; - if (file == null) - continue; - - using var contentStream = RealmFileStore.Storage.GetStream(file.File.GetStoragePath()); - using var contentStreamReader = new LineBufferedReader(contentStream); - var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader); - - using var skinStream = RealmFileStore.Storage.GetStream(file.File.GetStoragePath()); - using var skinStreamReader = new LineBufferedReader(contentStream); - var beatmapSkin = new LegacySkin(new SkinInfo(), null!) - { - Configuration = new LegacySkinDecoder().Decode(skinStreamReader) - }; - - Realm.Realm.Write(realm => - { - using var stream = new MemoryStream(); - convertAndEncodeLegacyBeatmap(beatmapContent, beatmapSkin, stream); - - beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); - beatmapInfo.Hash = stream.ComputeSHA2Hash(); - - file.File = RealmFileStore.Add(stream, realm).Detach(); - }); - } - - return legacyBeatmapExporter.ExportAsync(new RealmLiveUnmanaged(clone)); - } - - private void convertAndEncodeLegacyBeatmap(IBeatmap beatmapContent, ISkin beatmapSkin, Stream stream) - { - // Convert beatmap elements to be compatible with legacy format - // So we truncate time and position values to integers, and convert paths with multiple segments to bezier curves - foreach (var controlPoint in beatmapContent.ControlPointInfo.AllControlPoints) - controlPoint.Time = Math.Floor(controlPoint.Time); - - foreach (var hitObject in beatmapContent.HitObjects) - { - hitObject.StartTime = Math.Floor(hitObject.StartTime); - - if (hitObject is not IHasPath hasPath || BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue; - - var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); - - // Truncate control points to integer positions - foreach (var pathControlPoint in newControlPoints) - { - pathControlPoint.Position = new Vector2( - (float)Math.Floor(pathControlPoint.Position.X), - (float)Math.Floor(pathControlPoint.Position.Y)); - } - - hasPath.Path.ControlPoints.Clear(); - hasPath.Path.ControlPoints.AddRange(newControlPoints); - } - - // Encode to legacy format - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); - - stream.Seek(0, SeekOrigin.Begin); - } + public Task ExportLegacy(BeatmapSetInfo beatmap) => legacyBeatmapExporter.ExportAsync(beatmap.ToLive(Realm)); private void updateHashAndMarkDirty(BeatmapSetInfo setInfo) { diff --git a/osu.Game/Database/LegacyArchiveExporter.cs b/osu.Game/Database/LegacyArchiveExporter.cs index 7689ffc13d..9805207591 100644 --- a/osu.Game/Database/LegacyArchiveExporter.cs +++ b/osu.Game/Database/LegacyArchiveExporter.cs @@ -39,7 +39,7 @@ namespace osu.Game.Database { cancellationToken.ThrowIfCancellationRequested(); - using (var stream = UserFileStorage.GetStream(file.File.GetStoragePath())) + using (var stream = GetFileContents(model, file)) { if (stream == null) { @@ -65,5 +65,7 @@ namespace osu.Game.Database } } } + + protected virtual Stream? GetFileContents(TModel model, INamedFileUsage file) => UserFileStorage.GetStream(file.File.GetStoragePath()); } } diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 62096f862a..42d8a72073 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -1,13 +1,25 @@ // 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.IO; +using System.Linq; +using System.Text; using osu.Framework.Platform; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.Extensions; +using osu.Game.IO; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Skinning; +using osuTK; namespace osu.Game.Database { /// /// Exporter for osu!stable legacy beatmap archives. + /// Converts all beatmaps in the set to legacy format and exports it as a legacy package. /// public class LegacyBeatmapExporter : LegacyArchiveExporter { @@ -16,6 +28,64 @@ namespace osu.Game.Database { } + protected override Stream? GetFileContents(BeatmapSetInfo model, INamedFileUsage file) + { + bool isBeatmap = model.Beatmaps.Any(o => o.Hash == file.File.Hash); + + if (!isBeatmap) + return base.GetFileContents(model, file); + + // Read the beatmap contents and skin + using var contentStream = UserFileStorage.GetStream(file.File.GetStoragePath()); + + if (contentStream == null) + return null; + + using var contentStreamReader = new LineBufferedReader(contentStream); + var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader); + + using var skinStream = UserFileStorage.GetStream(file.File.GetStoragePath()); + using var skinStreamReader = new LineBufferedReader(contentStream); + var beatmapSkin = new LegacySkin(new SkinInfo(), null!) + { + Configuration = new LegacySkinDecoder().Decode(skinStreamReader) + }; + + // Convert beatmap elements to be compatible with legacy format + // So we truncate time and position values to integers, and convert paths with multiple segments to bezier curves + foreach (var controlPoint in beatmapContent.ControlPointInfo.AllControlPoints) + controlPoint.Time = Math.Floor(controlPoint.Time); + + foreach (var hitObject in beatmapContent.HitObjects) + { + hitObject.StartTime = Math.Floor(hitObject.StartTime); + + if (hitObject is not IHasPath hasPath || BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue; + + var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); + + // Truncate control points to integer positions + foreach (var pathControlPoint in newControlPoints) + { + pathControlPoint.Position = new Vector2( + (float)Math.Floor(pathControlPoint.Position.X), + (float)Math.Floor(pathControlPoint.Position.Y)); + } + + hasPath.Path.ControlPoints.Clear(); + hasPath.Path.ControlPoints.AddRange(newControlPoints); + } + + // Encode to legacy format + var stream = new MemoryStream(); + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); + + stream.Seek(0, SeekOrigin.Begin); + + return stream; + } + protected override string FileExtension => @".osz"; } } diff --git a/osu.Game/Database/ModelManager.cs b/osu.Game/Database/ModelManager.cs index 172a2df5a3..7d1dc5239a 100644 --- a/osu.Game/Database/ModelManager.cs +++ b/osu.Game/Database/ModelManager.cs @@ -25,11 +25,11 @@ namespace osu.Game.Database protected RealmAccess Realm { get; } - protected readonly RealmFileStore RealmFileStore; + private readonly RealmFileStore realmFileStore; public ModelManager(Storage storage, RealmAccess realm) { - RealmFileStore = new RealmFileStore(realm, storage); + realmFileStore = new RealmFileStore(realm, storage); Realm = realm; } @@ -77,7 +77,7 @@ namespace osu.Game.Database /// public void ReplaceFile(RealmNamedFileUsage file, Stream contents, Realm realm) { - file.File = RealmFileStore.Add(contents, realm); + file.File = realmFileStore.Add(contents, realm); } /// @@ -93,7 +93,7 @@ namespace osu.Game.Database return; } - var file = RealmFileStore.Add(contents, realm); + var file = realmFileStore.Add(contents, realm); var namedUsage = new RealmNamedFileUsage(file, filename); item.Files.Add(namedUsage); From d62cfc16166ff48f07778a67d37fe939e71e80c0 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Wed, 12 Jul 2023 20:24:09 -0400 Subject: [PATCH 0635/2100] Parse emoji to an empty string --- osu.Game/Online/Chat/MessageFormatter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 6ca651bc87..3e03cc287b 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -256,6 +256,9 @@ namespace osu.Game.Online.Chat private static MessageFormatterResult format(string toFormat, int startIndex = 0, int space = 3) { + // see: https://github.com/ppy/osu/pull/24190 + toFormat = Regex.Replace(toFormat, emoji_regex.ToString(), string.Empty); + var result = new MessageFormatterResult(toFormat); // handle the [link display] format From 5a43de1ace9e81e8e838c077f4fc79d5dbb1d229 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Wed, 12 Jul 2023 21:13:10 -0400 Subject: [PATCH 0636/2100] Update test cases --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 3c35dc311f..fef8054c70 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -478,7 +478,7 @@ namespace osu.Game.Tests.Chat Content = "This is a [http://www.simple-test.com simple test] with some [traps] and [[wiki links]]. Don't forget to visit https://osu.ppy.sh (now!)[http://google.com]\uD83D\uDE12" }); - Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now!\0\0\0", result.DisplayContent); + Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now!", result.DisplayContent); Assert.AreEqual(5, result.Links.Count); Link f = result.Links.Find(l => l.Url == "https://dev.ppy.sh/wiki/wiki links"); @@ -508,6 +508,7 @@ namespace osu.Game.Tests.Chat } [Test] + [Ignore("https://github.com/ppy/osu/pull/24190")] public void TestEmoji() { Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" }); From 3d256acfef02706afcb51316161314dd46edd19d Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Wed, 12 Jul 2023 21:40:49 -0400 Subject: [PATCH 0637/2100] Delete emoji test in TestLinkComplex --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index fef8054c70..5c063a5c6c 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -500,11 +500,6 @@ namespace osu.Game.Tests.Chat Assert.That(f, Is.Not.Null); Assert.AreEqual(78, f.Index); Assert.AreEqual(18, f.Length); - - f = result.Links.Find(l => l.Url == "\uD83D\uDE12"); - Assert.That(f, Is.Not.Null); - Assert.AreEqual(101, f.Index); - Assert.AreEqual(3, f.Length); } [Test] From f44e6e510d967792e55b86b7163a9d70addcf499 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Wed, 12 Jul 2023 22:42:12 -0400 Subject: [PATCH 0638/2100] 5 -> 4 --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 5c063a5c6c..aa45e360e6 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -479,7 +479,7 @@ namespace osu.Game.Tests.Chat }); Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now!", result.DisplayContent); - Assert.AreEqual(5, result.Links.Count); + Assert.AreEqual(4, result.Links.Count); Link f = result.Links.Find(l => l.Url == "https://dev.ppy.sh/wiki/wiki links"); Assert.That(f, Is.Not.Null); From 8e294c325842430f56189ef8142a4b288279c07f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 13:22:45 +0900 Subject: [PATCH 0639/2100] Add test coverage of hitting objects immediately after a swell --- .../Judgements/TestSceneSwellJudgements.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs index ccc829f09e..4abad98eab 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs @@ -114,5 +114,75 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements AddAssert("all tick offsets are 0", () => JudgementResults.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0)); } + + /// + /// Ensure input is correctly sent to subsequent hits if a swell is fully completed. + /// + [Test] + public void TestHitSwellThenHitHit() + { + const double swell_time = 1000; + const double hit_time = 1150; + + Swell swell = new Swell + { + StartTime = swell_time, + Duration = 100, + RequiredHits = 1 + }; + + Hit hit = new Hit + { + StartTime = hit_time + }; + + List frames = new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(swell_time, TaikoAction.LeftRim), + new TaikoReplayFrame(hit_time, TaikoAction.RightCentre), + }; + + PerformTest(frames, CreateBeatmap(swell, hit)); + + AssertJudgementCount(3); + + AssertResult(0, HitResult.IgnoreHit); + AssertResult(0, HitResult.LargeBonus); + AssertResult(0, HitResult.Great); + } + + [Test] + public void TestMissSwellThenHitHit() + { + const double swell_time = 1000; + const double hit_time = 1150; + + Swell swell = new Swell + { + StartTime = swell_time, + Duration = 100, + RequiredHits = 1 + }; + + Hit hit = new Hit + { + StartTime = hit_time + }; + + List frames = new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(hit_time, TaikoAction.RightCentre), + }; + + PerformTest(frames, CreateBeatmap(swell, hit)); + + AssertJudgementCount(3); + + AssertResult(0, HitResult.IgnoreMiss); + AssertResult(0, HitResult.IgnoreMiss); + AssertResult(0, HitResult.Great); + } } } From 259ac6d427b3142a99c0366024adb068b97cb47e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 13:16:58 +0900 Subject: [PATCH 0640/2100] Fix osu!taiko swells eating input after already being judged --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 8441e3a749..3fa6f4b756 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -276,6 +276,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (Time.Current < HitObject.StartTime) return false; + if (AllJudged) + return false; + bool isCentre = e.Action == TaikoAction.LeftCentre || e.Action == TaikoAction.RightCentre; // Ensure alternating centre and rim hits From fbf14a0f7cbd4c9b354c71affb49d8e0406c8584 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 13:41:35 +0900 Subject: [PATCH 0641/2100] Allow autoplay to fail Feels more correct. --- osu.Game/Rulesets/Mods/ModAutoplay.cs | 4 +--- osu.Game/Screens/Play/ReplayPlayer.cs | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 83afda3a28..ab2c84bada 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -11,7 +11,7 @@ using osu.Game.Replays; namespace osu.Game.Rulesets.Mods { - public abstract class ModAutoplay : Mod, IApplicableFailOverride, ICreateReplayData + public abstract class ModAutoplay : Mod, ICreateReplayData { public override string Name => "Autoplay"; public override string Acronym => "AT"; @@ -20,8 +20,6 @@ namespace osu.Game.Rulesets.Mods public override LocalisableString Description => "Watch a perfect automated play through the song."; public override double ScoreMultiplier => 1; - public bool PerformFail() => false; - public bool RestartOnFail => false; public override bool UserPlayable => false; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 8a4e63d21c..ca71a89b48 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using osu.Framework.Bindables; using osu.Framework.Input.Bindings; @@ -30,7 +31,7 @@ namespace osu.Game.Screens.Play // Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108) protected override bool CheckModsAllowFailure() { - if (!replayIsFailedScore) + if (!replayIsFailedScore && !GameplayState.Mods.OfType().Any()) return false; return base.CheckModsAllowFailure(); From 1bfe5a18cb94de911bb6ecf445abc8d32fe254ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 13:46:50 +0900 Subject: [PATCH 0642/2100] Rename `RestoreDefaultValueButton` to `RevertToDefaultButton` Because I can't find it every time I search. --- .../Settings/TestSceneKeyBindingPanel.cs | 10 +++--- ...n.cs => TestSceneRevertToDefaultButton.cs} | 10 +++--- .../Visual/Settings/TestSceneSettingsItem.cs | 34 +++++++++---------- .../TestSceneModSelectOverlay.cs | 2 +- ...alueButton.cs => RevertToDefaultButton.cs} | 4 +-- .../Settings/Sections/Input/KeyBindingRow.cs | 2 +- osu.Game/Overlays/Settings/SettingsItem.cs | 2 +- 7 files changed, 32 insertions(+), 32 deletions(-) rename osu.Game.Tests/Visual/Settings/{TestSceneRestoreDefaultValueButton.cs => TestSceneRevertToDefaultButton.cs} (82%) rename osu.Game/Overlays/{RestoreDefaultValueButton.cs => RevertToDefaultButton.cs} (97%) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index da48086717..449ca0f258 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -195,16 +195,16 @@ namespace osu.Game.Tests.Visual.Settings InputManager.ReleaseKey(Key.P); }); - AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha > 0); + AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha > 0); AddStep("click reset button for bindings", () => { - var resetButton = settingsKeyBindingRow.ChildrenOfType>().First(); + var resetButton = settingsKeyBindingRow.ChildrenOfType>().First(); resetButton.TriggerClick(); }); - AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0); + AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0); AddAssert("binding cleared", () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0))); @@ -225,7 +225,7 @@ namespace osu.Game.Tests.Visual.Settings InputManager.ReleaseKey(Key.P); }); - AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha > 0); + AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha > 0); AddStep("click reset button for bindings", () => { @@ -234,7 +234,7 @@ namespace osu.Game.Tests.Visual.Settings resetButton.TriggerClick(); }); - AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0); + AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0); AddAssert("binding cleared", () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0))); diff --git a/osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs b/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs similarity index 82% rename from osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs rename to osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs index 6e52881f5e..609283edfc 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Tests.Visual.Settings { - public partial class TestSceneRestoreDefaultValueButton : OsuTestScene + public partial class TestSceneRevertToDefaultButton : OsuTestScene { [Resolved] private OsuColour colours { get; set; } @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Settings [Test] public void TestBasic() { - RestoreDefaultValueButton restoreDefaultValueButton = null; + RevertToDefaultButton revertToDefaultButton = null; AddStep("create button", () => Child = new Container { @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Settings RelativeSizeAxes = Axes.Both, Colour = colours.GreySeaFoam }, - restoreDefaultValueButton = new RestoreDefaultValueButton + revertToDefaultButton = new RevertToDefaultButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -55,8 +55,8 @@ namespace osu.Game.Tests.Visual.Settings AddSliderStep("set scale", 1, 4, 1, scale => { this.scale = scale; - if (restoreDefaultValueButton != null) - restoreDefaultValueButton.Scale = new Vector2(scale); + if (revertToDefaultButton != null) + revertToDefaultButton.Scale = new Vector2(scale); }); AddToggleStep("toggle default state", state => current.Value = state ? default : 1); AddToggleStep("toggle disabled state", state => current.Disabled = state); diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs index 384508f375..ec0ad685c5 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Settings public void TestRestoreDefaultValueButtonVisibility() { SettingsTextBox textBox = null; - RestoreDefaultValueButton restoreDefaultValueButton = null; + RevertToDefaultButton revertToDefaultButton = null; AddStep("create settings item", () => { @@ -33,22 +33,22 @@ namespace osu.Game.Tests.Visual.Settings }; }); AddUntilStep("wait for loaded", () => textBox.IsLoaded); - AddStep("retrieve restore default button", () => restoreDefaultValueButton = textBox.ChildrenOfType>().Single()); + AddStep("retrieve restore default button", () => revertToDefaultButton = textBox.ChildrenOfType>().Single()); - AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); + AddAssert("restore button hidden", () => revertToDefaultButton.Alpha == 0); AddStep("change value from default", () => textBox.Current.Value = "non-default"); - AddUntilStep("restore button shown", () => restoreDefaultValueButton.Alpha > 0); + AddUntilStep("restore button shown", () => revertToDefaultButton.Alpha > 0); AddStep("restore default", () => textBox.Current.SetDefault()); - AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); + AddUntilStep("restore button hidden", () => revertToDefaultButton.Alpha == 0); } [Test] public void TestSetAndClearLabelText() { SettingsTextBox textBox = null; - RestoreDefaultValueButton restoreDefaultValueButton = null; + RevertToDefaultButton revertToDefaultButton = null; OsuTextBox control = null; AddStep("create settings item", () => @@ -61,25 +61,25 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("wait for loaded", () => textBox.IsLoaded); AddStep("retrieve components", () => { - restoreDefaultValueButton = textBox.ChildrenOfType>().Single(); + revertToDefaultButton = textBox.ChildrenOfType>().Single(); control = textBox.ChildrenOfType().Single(); }); - AddStep("set non-default value", () => restoreDefaultValueButton.Current.Value = "non-default"); - AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1)); + AddStep("set non-default value", () => revertToDefaultButton.Current.Value = "non-default"); + AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent.DrawHeight, control.DrawHeight, 1)); AddStep("set label", () => textBox.LabelText = "label text"); AddAssert("default value button centre aligned to label size", () => { var label = textBox.ChildrenOfType().Single(spriteText => spriteText.Text == "label text"); - return Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, label.DrawHeight, 1); + return Precision.AlmostEquals(revertToDefaultButton.Parent.DrawHeight, label.DrawHeight, 1); }); AddStep("clear label", () => textBox.LabelText = default); - AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1)); + AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent.DrawHeight, control.DrawHeight, 1)); AddStep("set warning text", () => textBox.SetNoticeText("This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator...", true)); - AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1)); + AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent.DrawHeight, control.DrawHeight, 1)); } /// @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Settings { BindableFloat current = null; SettingsSlider sliderBar = null; - RestoreDefaultValueButton restoreDefaultValueButton = null; + RevertToDefaultButton revertToDefaultButton = null; AddStep("create settings item", () => { @@ -107,15 +107,15 @@ namespace osu.Game.Tests.Visual.Settings }; }); AddUntilStep("wait for loaded", () => sliderBar.IsLoaded); - AddStep("retrieve restore default button", () => restoreDefaultValueButton = sliderBar.ChildrenOfType>().Single()); + AddStep("retrieve restore default button", () => revertToDefaultButton = sliderBar.ChildrenOfType>().Single()); - AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); + AddAssert("restore button hidden", () => revertToDefaultButton.Alpha == 0); AddStep("change value to next closest", () => sliderBar.Current.Value += current.Precision * 0.6f); - AddUntilStep("restore button shown", () => restoreDefaultValueButton.Alpha > 0); + AddUntilStep("restore button shown", () => revertToDefaultButton.Alpha > 0); AddStep("restore default", () => sliderBar.Current.SetDefault()); - AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); + AddUntilStep("restore button hidden", () => revertToDefaultButton.Alpha == 0); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 4cb6899ebc..ad79865ad9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -793,7 +793,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick()); AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType().Single() - .ChildrenOfType>().Single().TriggerClick()); + .ChildrenOfType>().Single().TriggerClick()); AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(0.7)); } diff --git a/osu.Game/Overlays/RestoreDefaultValueButton.cs b/osu.Game/Overlays/RevertToDefaultButton.cs similarity index 97% rename from osu.Game/Overlays/RestoreDefaultValueButton.cs rename to osu.Game/Overlays/RevertToDefaultButton.cs index 97c66fdf02..fcd8b74009 100644 --- a/osu.Game/Overlays/RestoreDefaultValueButton.cs +++ b/osu.Game/Overlays/RevertToDefaultButton.cs @@ -22,7 +22,7 @@ using osu.Game.Localisation; namespace osu.Game.Overlays { - public partial class RestoreDefaultValueButton : OsuClickableContainer, IHasCurrentValue + public partial class RevertToDefaultButton : OsuClickableContainer, IHasCurrentValue { public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; @@ -58,7 +58,7 @@ namespace osu.Game.Overlays private CircularContainer circle = null!; private Box background = null!; - public RestoreDefaultValueButton() + public RevertToDefaultButton() : base(HoverSampleSet.Button) { } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 725925c8cf..1e2283b58b 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -103,7 +103,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { RelativeSizeAxes = Axes.Y, Width = SettingsPanel.CONTENT_MARGINS, - Child = new RestoreDefaultValueButton + Child = new RevertToDefaultButton { Current = isDefault, Action = RestoreDefaults, diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 5f4bb9d57f..9085b6c911 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -217,7 +217,7 @@ namespace osu.Game.Overlays.Settings // intentionally done before LoadComplete to avoid overhead. if (ShowsDefaultIndicator) { - defaultValueIndicatorContainer.Add(new RestoreDefaultValueButton + defaultValueIndicatorContainer.Add(new RevertToDefaultButton { Current = controlWithCurrent.Current, Anchor = Anchor.Centre, From 94201579f6d6307308cb9823a308d9bd9c6db5cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 14:26:01 +0900 Subject: [PATCH 0643/2100] Update design of "revert to default" button I keep getting feedback that the old design looked like anything *but* a button to revert defaults. Including people clicking it expecting opposite behaviour. This is intended to be a temporary design until we get the full new UI components online (where this is moved to the right-hand-side). --- .../TestSceneRevertToDefaultButton.cs | 13 ++- osu.Game/Overlays/RevertToDefaultButton.cs | 85 +++++++++---------- 2 files changed, 44 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs b/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs index 609283edfc..bfef120358 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs @@ -1,15 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - 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; -using osu.Game.Graphics; using osu.Game.Overlays; using osuTK; @@ -17,11 +14,11 @@ namespace osu.Game.Tests.Visual.Settings { public partial class TestSceneRevertToDefaultButton : OsuTestScene { - [Resolved] - private OsuColour colours { get; set; } - private float scale = 1; + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + private readonly Bindable current = new Bindable { Default = default, @@ -31,7 +28,7 @@ namespace osu.Game.Tests.Visual.Settings [Test] public void TestBasic() { - RevertToDefaultButton revertToDefaultButton = null; + RevertToDefaultButton revertToDefaultButton = null!; AddStep("create button", () => Child = new Container { @@ -41,7 +38,7 @@ namespace osu.Game.Tests.Visual.Settings new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.GreySeaFoam + Colour = colourProvider.Background2, }, revertToDefaultButton = new RevertToDefaultButton { diff --git a/osu.Game/Overlays/RevertToDefaultButton.cs b/osu.Game/Overlays/RevertToDefaultButton.cs index fcd8b74009..48491c5d9c 100644 --- a/osu.Game/Overlays/RevertToDefaultButton.cs +++ b/osu.Game/Overlays/RevertToDefaultButton.cs @@ -1,24 +1,20 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osuTK; using osu.Game.Localisation; +using osuTK; namespace osu.Game.Overlays { @@ -31,11 +27,17 @@ namespace osu.Game.Overlays // this is intentionally not using BindableWithCurrent, as it can use the wrong IsDefault implementation when passed a BindableNumber. // using GetBoundCopy() ensures that the received bindable is of the exact same type as the source bindable and uses the proper IsDefault implementation. - private Bindable current; + private Bindable? current; + + private SpriteIcon icon = null!; + private Circle circle = null!; + + [Resolved] + private OverlayColourProvider colours { get; set; } = null!; public Bindable Current { - get => current; + get => current.AsNonNull(); set { current?.UnbindAll(); @@ -50,43 +52,37 @@ namespace osu.Game.Overlays } } - [Resolved] - private OsuColour colours { get; set; } - - private const float size = 4; - - private CircularContainer circle = null!; - private Box background = null!; - public RevertToDefaultButton() : base(HoverSampleSet.Button) { } [BackgroundDependencyLoader] - private void load(OsuColour colour) + private void load() { // size intentionally much larger than actual drawn content, so that the button is easier to click. - Size = new Vector2(3 * size); + Size = new Vector2(14); - Add(circle = new CircularContainer + AddRange(new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(size), - Masking = true, - Child = background = new Box + circle = new Circle { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Colour = colour.Lime1 + }, + icon = new SpriteIcon + { + Icon = FontAwesome.Solid.Undo, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(8), } }); - Alpha = 0f; - Action += () => { - if (!current.Disabled) + if (current?.Disabled == false) current.SetDefault(); }; } @@ -120,28 +116,25 @@ namespace osu.Game.Overlays if (current == null) return; - Enabled.Value = !Current.Disabled; + Enabled.Value = !current.Disabled; - if (!Current.Disabled) + this.FadeTo(current.Disabled ? 0.2f : (Current.IsDefault ? 0 : 1), fade_duration, Easing.OutQuint); + + if (IsHovered && Enabled.Value) { - this.FadeTo(Current.IsDefault ? 0 : 1, fade_duration, Easing.OutQuint); - background.FadeColour(IsHovered ? colours.Lime0 : colours.Lime1, fade_duration, Easing.OutQuint); - circle.TweenEdgeEffectTo(new EdgeEffectParameters - { - Colour = (IsHovered ? colours.Lime1 : colours.Lime3).Opacity(0.4f), - Radius = IsHovered ? 8 : 4, - Type = EdgeEffectType.Glow - }, fade_duration, Easing.OutQuint); + icon.RotateTo(-40, 500, Easing.OutQuint); + + icon.FadeColour(colours.Light1, 300, Easing.OutQuint); + circle.FadeColour(colours.Background2, 300, Easing.OutQuint); + this.ScaleTo(1.2f, 300, Easing.OutQuint); } else { - background.FadeColour(colours.Lime3, fade_duration, Easing.OutQuint); - circle.TweenEdgeEffectTo(new EdgeEffectParameters - { - Colour = colours.Lime3.Opacity(0.1f), - Radius = 2, - Type = EdgeEffectType.Glow - }, fade_duration, Easing.OutQuint); + icon.RotateTo(0, 100, Easing.OutQuint); + + icon.FadeColour(colours.Colour0, 100, Easing.OutQuint); + circle.FadeColour(colours.Background3, 100, Easing.OutQuint); + this.ScaleTo(1f, 100, Easing.OutQuint); } } } From e2b5abd4e8e90a0cd6ef169f2fefaad7909a9376 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 14:43:14 +0900 Subject: [PATCH 0644/2100] Split bar drawable creation into own method --- .../HitEventTimingDistributionGraph.cs | 137 +++++++++--------- 1 file changed, 69 insertions(+), 68 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 16da8c64a0..7e7c0ccb54 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -113,94 +113,95 @@ namespace osu.Game.Screens.Ranking.Statistics } } - if (barDrawables != null) - { - for (int i = 0; i < barDrawables.Length; i++) - { - barDrawables[i].UpdateOffset(bins[i].Sum(b => b.Value)); - } - } + if (barDrawables == null) + createBarDrawables(); else { - int maxCount = bins.Max(b => b.Values.Sum()); - barDrawables = bins.Select((bin, i) => new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index)).ToArray(); + for (int i = 0; i < barDrawables.Length; i++) + barDrawables[i].UpdateOffset(bins[i].Sum(b => b.Value)); + } + } - Container axisFlow; + private void createBarDrawables() + { + int maxCount = bins.Max(b => b.Values.Sum()); + barDrawables = bins.Select((_, i) => new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index)).ToArray(); - const float axis_font_size = 12; + Container axisFlow; - InternalChild = new GridContainer + const float axis_font_size = 12; + + InternalChild = new GridContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Width = 0.8f, + Content = new[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Width = 0.8f, - Content = new[] + new Drawable[] { - new Drawable[] + new GridContainer { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] { barDrawables } - } - }, - new Drawable[] - { - axisFlow = new Container - { - RelativeSizeAxes = Axes.X, - Height = axis_font_size, - } - }, + RelativeSizeAxes = Axes.Both, + Content = new[] { barDrawables } + } }, - RowDimensions = new[] + new Drawable[] { - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - } - }; + axisFlow = new Container + { + RelativeSizeAxes = Axes.X, + Height = axis_font_size, + } + }, + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + } + }; - // Our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. - double maxValue = timing_distribution_bins * binSize; - double axisValueStep = maxValue / axis_points; + // Our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. + double maxValue = timing_distribution_bins * binSize; + double axisValueStep = maxValue / axis_points; + + axisFlow.Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "0", + Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) + }); + + for (int i = 1; i <= axis_points; i++) + { + double axisValue = i * axisValueStep; + float position = (float)(axisValue / maxValue); + float alpha = 1f - position * 0.8f; axisFlow.Add(new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = "0", + RelativePositionAxes = Axes.X, + X = -position / 2, + Alpha = alpha, + Text = axisValue.ToString("-0"), Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) }); - for (int i = 1; i <= axis_points; i++) + axisFlow.Add(new OsuSpriteText { - double axisValue = i * axisValueStep; - float position = (float)(axisValue / maxValue); - float alpha = 1f - position * 0.8f; - - axisFlow.Add(new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - X = -position / 2, - Alpha = alpha, - Text = axisValue.ToString("-0"), - Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) - }); - - axisFlow.Add(new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - X = position / 2, - Alpha = alpha, - Text = axisValue.ToString("+0"), - Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) - }); - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + X = position / 2, + Alpha = alpha, + Text = axisValue.ToString("+0"), + Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) + }); } } From 21f26f98da76ee5d89dc7c5c8e32309bd2caf034 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 14:57:01 +0900 Subject: [PATCH 0645/2100] Fix graph breaking when resized vertically --- .../HitEventTimingDistributionGraph.cs | 60 +++++++++++++------ 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 7e7c0ccb54..85c2777645 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -5,9 +5,11 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Layout; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Scoring; @@ -220,6 +222,8 @@ namespace osu.Game.Screens.Ranking.Statistics private Circle? boxAdjustment; + private float? lastDrawHeight; + [Resolved] private OsuColour colours { get; set; } = null!; @@ -276,24 +280,18 @@ namespace osu.Game.Screens.Ranking.Statistics { base.LoadComplete(); - float offsetValue = 0; + Scheduler.AddOnce(updateMetrics, true); + } - for (int i = 0; i < boxOriginals.Length; i++) + protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) + { + if (invalidation.HasFlagFast(Invalidation.DrawSize)) { - int value = i < values.Count ? values[i].Value : 0; - - var box = boxOriginals[i]; - - box.Y = 0; - box.Height = 0; - - box.MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint); - box.ResizeHeightTo(heightForValue(value), duration, Easing.OutQuint); - offsetValue -= value; + if (lastDrawHeight != null && lastDrawHeight != DrawHeight) + Scheduler.AddOnce(updateMetrics, false); } - if (boxAdjustment != null) - drawAdjustmentBar(); + return base.OnInvalidate(invalidation, source); } public void UpdateOffset(float adjustment) @@ -318,12 +316,36 @@ namespace osu.Game.Screens.Ranking.Statistics } offsetAdjustment = adjustment; - drawAdjustmentBar(); + + Scheduler.AddOnce(updateMetrics, true); } - private float offsetForValue(float value) => (1 - minimum_height) * value / maxValue; + private void updateMetrics(bool animate = true) + { + float offsetValue = 0; - private float heightForValue(float value) => minimum_height + offsetForValue(value); + for (int i = 0; i < boxOriginals.Length; i++) + { + int value = i < values.Count ? values[i].Value : 0; + + var box = boxOriginals[i]; + + box.Y = 0; + box.Height = 0; + + box.MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint); + box.ResizeHeightTo(heightForValue(value), duration, Easing.OutQuint); + offsetValue -= value; + } + + if (boxAdjustment != null) + drawAdjustmentBar(); + + if (!animate) + FinishTransforms(true); + + lastDrawHeight = DrawHeight; + } private void drawAdjustmentBar() { @@ -332,6 +354,10 @@ namespace osu.Game.Screens.Ranking.Statistics boxAdjustment.ResizeHeightTo(heightForValue(offsetAdjustment), duration, Easing.OutQuint); boxAdjustment.FadeTo(!hasAdjustment ? 0 : 1, duration, Easing.OutQuint); } + + private float offsetForValue(float value) => (1 - minimum_height) * value / maxValue; + + private float heightForValue(float value) => minimum_height + offsetForValue(value); } } } From 1d62a041ccdd8ae10066823080b76861d42ff15d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 15:07:26 +0900 Subject: [PATCH 0646/2100] Fix animation restarting unexpectedly --- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 85c2777645..e8d2eefd81 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -330,9 +330,6 @@ namespace osu.Game.Screens.Ranking.Statistics var box = boxOriginals[i]; - box.Y = 0; - box.Height = 0; - box.MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint); box.ResizeHeightTo(heightForValue(value), duration, Easing.OutQuint); offsetValue -= value; From 3d17a03dc696c340ca690716a380f83a500e3013 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Thu, 13 Jul 2023 01:09:35 -0400 Subject: [PATCH 0647/2100] Emojis now represented as "[emoji]" --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 7 +++---- osu.Game/Online/Chat/MessageFormatter.cs | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index aa45e360e6..529b28318a 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -478,7 +478,7 @@ namespace osu.Game.Tests.Chat Content = "This is a [http://www.simple-test.com simple test] with some [traps] and [[wiki links]]. Don't forget to visit https://osu.ppy.sh (now!)[http://google.com]\uD83D\uDE12" }); - Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now!", result.DisplayContent); + Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now![emoji]", result.DisplayContent); Assert.AreEqual(4, result.Links.Count); Link f = result.Links.Find(l => l.Url == "https://dev.ppy.sh/wiki/wiki links"); @@ -503,11 +503,10 @@ namespace osu.Game.Tests.Chat } [Test] - [Ignore("https://github.com/ppy/osu/pull/24190")] public void TestEmoji() { - Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" }); - Assert.AreEqual("Hello world\0\0\0<--This is an emoji,There are more:\0\0\0\0\0\0,\0\0\0", result.DisplayContent); + Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more emojis among us:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" }); + Assert.AreEqual("Hello world[emoji]<--This is an emoji,There are more emojis among us:[emoji][emoji],[emoji]", result.DisplayContent); Assert.AreEqual(result.Links.Count, 4); Assert.AreEqual(result.Links[0].Index, 11); Assert.AreEqual(result.Links[1].Index, 49); diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 3e03cc287b..7a3941038e 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -257,7 +257,7 @@ namespace osu.Game.Online.Chat private static MessageFormatterResult format(string toFormat, int startIndex = 0, int space = 3) { // see: https://github.com/ppy/osu/pull/24190 - toFormat = Regex.Replace(toFormat, emoji_regex.ToString(), string.Empty); + toFormat = Regex.Replace(toFormat, emoji_regex.ToString(), "[emoji]"); var result = new MessageFormatterResult(toFormat); From 2f40989a4fb67af56c9e7685794bb5ff42aad2f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 17:25:08 +0900 Subject: [PATCH 0648/2100] Allow no fail mod during autoplay --- osu.Game/Rulesets/Mods/ModAutoplay.cs | 2 +- osu.Game/Rulesets/Mods/ModNoFail.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index ab2c84bada..a4ffbeacef 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mods public override bool ValidForMultiplayer => false; public override bool ValidForMultiplayerAsFreeMod => false; - public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAdaptiveSpeed) }; + public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModAdaptiveSpeed) }; public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; diff --git a/osu.Game/Rulesets/Mods/ModNoFail.cs b/osu.Game/Rulesets/Mods/ModNoFail.cs index 31bb4338b3..8c61d948a4 100644 --- a/osu.Game/Rulesets/Mods/ModNoFail.cs +++ b/osu.Game/Rulesets/Mods/ModNoFail.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyReduction; public override LocalisableString Description => "You can't fail, no matter what."; public override double ScoreMultiplier => 0.5; - public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModAutoplay) }; + public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition) }; } } From 403a7aa0006d7103100b02344665918526462df9 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Thu, 13 Jul 2023 04:28:35 -0400 Subject: [PATCH 0649/2100] 0 links are expected --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 529b28318a..4d18a3d0bb 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -507,7 +507,7 @@ namespace osu.Game.Tests.Chat { Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more emojis among us:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" }); Assert.AreEqual("Hello world[emoji]<--This is an emoji,There are more emojis among us:[emoji][emoji],[emoji]", result.DisplayContent); - Assert.AreEqual(result.Links.Count, 4); + Assert.AreEqual(result.Links.Count, 0); Assert.AreEqual(result.Links[0].Index, 11); Assert.AreEqual(result.Links[1].Index, 49); Assert.AreEqual(result.Links[2].Index, 52); From ea6704ca1d528df6b94028aa2f82836e16947afe Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Thu, 13 Jul 2023 04:41:53 -0400 Subject: [PATCH 0650/2100] Move parsing --- osu.Game/Online/Chat/MessageFormatter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 7a3941038e..10a005d71a 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -256,9 +256,6 @@ namespace osu.Game.Online.Chat private static MessageFormatterResult format(string toFormat, int startIndex = 0, int space = 3) { - // see: https://github.com/ppy/osu/pull/24190 - toFormat = Regex.Replace(toFormat, emoji_regex.ToString(), "[emoji]"); - var result = new MessageFormatterResult(toFormat); // handle the [link display] format @@ -282,6 +279,9 @@ namespace osu.Game.Online.Chat // handle channels handleMatches(channel_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}chan/{{0}}", result, startIndex, LinkAction.OpenChannel); + // see: https://github.com/ppy/osu/pull/24190 + result.Text = Regex.Replace(result.Text, emoji_regex.ToString(), "[emoji]"); + return result; } From f5c472c0fe8a2a2bd9f4d9305be46353119dfce0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 17:36:52 +0900 Subject: [PATCH 0651/2100] Don't hide mod select overlay when pressing select binding with no search --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 2 +- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 4cb6899ebc..9561a8875c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -542,7 +542,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("clear search", () => modSelectOverlay.SearchTerm = string.Empty); AddStep("press enter", () => InputManager.Key(Key.Enter)); - AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden); + AddAssert("mod select still visible", () => modSelectOverlay.State.Value == Visibility.Visible); } [Test] diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 9035503723..c09668850a 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -48,7 +48,8 @@ namespace osu.Game.Overlays.Mods /// Contrary to and , the instances /// inside the objects are owned solely by this instance. /// - public Bindable>> AvailableMods { get; } = new Bindable>>(new Dictionary>()); + public Bindable>> AvailableMods { get; } = + new Bindable>>(new Dictionary>()); private Func isValidMod = _ => true; @@ -636,12 +637,9 @@ namespace osu.Game.Overlays.Mods case GlobalAction.Select: { - // Pressing select should select first filtered mod or completely hide the overlay in one shot if search term is empty. + // Pressing select should select first filtered mod if a search is in progress. if (string.IsNullOrEmpty(SearchTerm)) - { - hideOverlay(true); return true; - } ModState? firstMod = columnFlow.Columns.OfType().FirstOrDefault(m => m.IsPresent)?.AvailableMods.FirstOrDefault(x => x.Visible); From db37de45ac12ebb0ceb9dda45e40d2e75f92c81c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 17:49:06 +0900 Subject: [PATCH 0652/2100] Allow saving changes to presets in popover using "select" binding --- .../UserInterface/TestSceneModPresetColumn.cs | 9 ++---- .../Graphics/UserInterfaceV2/OsuPopover.cs | 2 +- osu.Game/Overlays/Mods/AddPresetPopover.cs | 14 ++++++++++ osu.Game/Overlays/Mods/EditPresetPopover.cs | 28 ++++++++++++++----- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index bf6d8e524f..1779b240cc 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -9,8 +9,8 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Testing; using osu.Framework.Graphics.Cursor; +using osu.Framework.Testing; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; @@ -304,11 +304,8 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("preset is not changed", () => panel.Preset.Value.Name == presetName); AddUntilStep("popover is unchanged", () => this.ChildrenOfType().FirstOrDefault() == popover); AddStep("edit preset name", () => popover.ChildrenOfType().First().Current.Value = "something new"); - AddStep("attempt preset edit", () => - { - InputManager.MoveMouseTo(popover.ChildrenOfType().ElementAt(1)); - InputManager.Click(MouseButton.Left); - }); + AddStep("commit changes to textbox", () => InputManager.Key(Key.Enter)); + AddStep("attempt preset edit via select binding", () => InputManager.Key(Key.Enter)); AddUntilStep("popover closed", () => !this.ChildrenOfType().Any()); AddAssert("preset is changed", () => panel.Preset.Value.Name != presetName); } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs index 00e5b8838c..381193d539 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs @@ -65,7 +65,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 return base.OnKeyDown(e); } - public bool OnPressed(KeyBindingPressEvent e) + public virtual bool OnPressed(KeyBindingPressEvent e) { if (e.Repeat) return false; diff --git a/osu.Game/Overlays/Mods/AddPresetPopover.cs b/osu.Game/Overlays/Mods/AddPresetPopover.cs index ef855f6166..638592a9b5 100644 --- a/osu.Game/Overlays/Mods/AddPresetPopover.cs +++ b/osu.Game/Overlays/Mods/AddPresetPopover.cs @@ -8,10 +8,12 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -95,6 +97,18 @@ namespace osu.Game.Overlays.Mods }, true); } + public override bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.Select: + createButton.TriggerClick(); + return true; + } + + return base.OnPressed(e); + } + private void createPreset() { realm.Write(r => r.Add(new ModPreset diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 5220f6a391..d755825a95 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -8,11 +8,13 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osuTK; @@ -130,6 +132,25 @@ namespace osu.Game.Overlays.Mods }, true); } + protected override void LoadComplete() + { + base.LoadComplete(); + + ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox)); + } + + public override bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.Select: + saveButton.TriggerClick(); + return true; + } + + return base.OnPressed(e); + } + private void useCurrentMods() { saveableMods = selectedMods.Value.ToHashSet(); @@ -150,13 +171,6 @@ namespace osu.Game.Overlays.Mods return !saveableMods.SetEquals(selectedMods.Value); } - protected override void LoadComplete() - { - base.LoadComplete(); - - ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox)); - } - private void save() { preset.PerformWrite(s => From 71351d898247d0329dca7726e0bc3965edb8dffd Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Thu, 13 Jul 2023 05:06:23 -0400 Subject: [PATCH 0653/2100] I forgor to remove these --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 4d18a3d0bb..46dc47bf53 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -508,14 +508,6 @@ namespace osu.Game.Tests.Chat Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more emojis among us:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" }); Assert.AreEqual("Hello world[emoji]<--This is an emoji,There are more emojis among us:[emoji][emoji],[emoji]", result.DisplayContent); Assert.AreEqual(result.Links.Count, 0); - Assert.AreEqual(result.Links[0].Index, 11); - Assert.AreEqual(result.Links[1].Index, 49); - Assert.AreEqual(result.Links[2].Index, 52); - Assert.AreEqual(result.Links[3].Index, 56); - Assert.AreEqual(result.Links[0].Url, "\uD83D\uDE12"); - Assert.AreEqual(result.Links[1].Url, "\uD83D\uDE10"); - Assert.AreEqual(result.Links[2].Url, "\uD83D\uDE00"); - Assert.AreEqual(result.Links[3].Url, "\uD83D\uDE20"); } [Test] From 6529c810fadf3c2c25dec35441ca7f72d2448bb3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 18:17:46 +0900 Subject: [PATCH 0654/2100] Change order and test population in `TestSceneStatisticsPanel` to give better visible results --- .../Visual/Ranking/TestSceneStatisticsPanel.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 67211a3b72..69c9fcb637 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -30,19 +30,19 @@ namespace osu.Game.Tests.Visual.Ranking public partial class TestSceneStatisticsPanel : OsuTestScene { [Test] - public void TestScoreWithTimeStatistics() + public void TestScoreWithPositionStatistics() { var score = TestResources.CreateTestScoreInfo(); - score.HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); + score.HitEvents = createPositionDistributedHitEvents(); loadPanel(score); } [Test] - public void TestScoreWithPositionStatistics() + public void TestScoreWithTimeStatistics() { var score = TestResources.CreateTestScoreInfo(); - score.HitEvents = createPositionDistributedHitEvents(); + score.HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); loadPanel(score); } @@ -89,18 +89,19 @@ namespace osu.Game.Tests.Visual.Ranking private static List createPositionDistributedHitEvents() { - var hitEvents = new List(); + var hitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); + // Use constant seed for reproducibility var random = new Random(0); - for (int i = 0; i < 500; i++) + for (int i = 0; i < hitEvents.Count; i++) { double angle = random.NextDouble() * 2 * Math.PI; double radius = random.NextDouble() * 0.5f * OsuHitObject.OBJECT_RADIUS; var position = new Vector2((float)(radius * Math.Cos(angle)), (float)(radius * Math.Sin(angle))); - hitEvents.Add(new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), position)); + hitEvents[i] = hitEvents[i].With(position); } return hitEvents; From b7ab46d87b538725086ab4d556f4f19d4118ddd5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 01:06:26 +0900 Subject: [PATCH 0655/2100] Add full statistics score to `TestSceneResultsScreen` --- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 5 ++++- osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index c5b61c1a90..4eba60de3c 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -82,7 +82,10 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both }; - stack.Push(screen = createResultsScreen()); + var score = TestResources.CreateTestScoreInfo(); + score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); + + stack.Push(screen = createResultsScreen(score)); }); AddUntilStep("wait for loaded", () => screen.IsLoaded); AddAssert("retry overlay not present", () => screen.RetryOverlay == null); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 69c9fcb637..e02782678f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking public void TestScoreWithPositionStatistics() { var score = TestResources.CreateTestScoreInfo(); - score.HitEvents = createPositionDistributedHitEvents(); + score.HitEvents = CreatePositionDistributedHitEvents(); loadPanel(score); } @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Ranking }; }); - private static List createPositionDistributedHitEvents() + public static List CreatePositionDistributedHitEvents() { var hitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); From aebbffacf247adf4d9e64e3205c5c5c421032685 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 19:06:48 +0900 Subject: [PATCH 0656/2100] Show online stats on `TestSceneStatisticsPanel` Fix test regression --- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 1 + osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 4eba60de3c..937bbe1448 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -83,6 +83,7 @@ namespace osu.Game.Tests.Visual.Ranking }; var score = TestResources.CreateTestScoreInfo(); + score.OnlineID = 1234; score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); stack.Push(screen = createResultsScreen(score)); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index e02782678f..e0d743b046 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Online.Solo; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; @@ -23,6 +24,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Tests.Resources; +using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -33,6 +35,7 @@ namespace osu.Game.Tests.Visual.Ranking public void TestScoreWithPositionStatistics() { var score = TestResources.CreateTestScoreInfo(); + score.OnlineID = 1234; score.HitEvents = CreatePositionDistributedHitEvents(); loadPanel(score); @@ -79,11 +82,12 @@ namespace osu.Game.Tests.Visual.Ranking private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { - Child = new StatisticsPanel + Child = new SoloStatisticsPanel(score) { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, - Score = { Value = score } + Score = { Value = score }, + StatisticsUpdate = { Value = new SoloStatisticsUpdate(score, new UserStatistics(), new UserStatistics()) } }; }); From 654a7057fca18ba68bb8590b34b4a06076d014ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 01:29:58 +0900 Subject: [PATCH 0657/2100] Add actual statistics changes to better visualise layout --- .../Ranking/TestSceneStatisticsPanel.cs | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index e0d743b046..93005271a9 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -87,7 +87,44 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, Score = { Value = score }, - StatisticsUpdate = { Value = new SoloStatisticsUpdate(score, new UserStatistics(), new UserStatistics()) } + StatisticsUpdate = + { + Value = new SoloStatisticsUpdate(score, new UserStatistics + { + Level = new UserStatistics.LevelInfo + { + Current = 5, + Progress = 20, + }, + GlobalRank = 38000, + CountryRank = 12006, + PP = 2134, + RankedScore = 21123849, + Accuracy = 0.985, + PlayCount = 13375, + PlayTime = 354490, + TotalScore = 128749597, + TotalHits = 0, + MaxCombo = 1233, + }, new UserStatistics + { + Level = new UserStatistics.LevelInfo + { + Current = 5, + Progress = 30, + }, + GlobalRank = 36000, + CountryRank = 12000, + PP = (decimal)2134.5, + RankedScore = 23897015, + Accuracy = 0.984, + PlayCount = 13376, + PlayTime = 35789, + TotalScore = 132218497, + TotalHits = 0, + MaxCombo = 1233, + }) + } }; }); From 6ef39b87fed8b44b5f9219da12d639ae5e4f422b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 01:24:09 +0900 Subject: [PATCH 0658/2100] Reorder tests for testability --- .../Visual/Ranking/TestSceneResultsScreen.cs | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 937bbe1448..1c904a936f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -69,6 +69,30 @@ namespace osu.Game.Tests.Visual.Ranking })); } + [TestCase(1, ScoreRank.X)] + [TestCase(0.9999, ScoreRank.S)] + [TestCase(0.975, ScoreRank.S)] + [TestCase(0.925, ScoreRank.A)] + [TestCase(0.85, ScoreRank.B)] + [TestCase(0.75, ScoreRank.C)] + [TestCase(0.5, ScoreRank.D)] + [TestCase(0.2, ScoreRank.D)] + public void TestResultsWithPlayer(double accuracy, ScoreRank rank) + { + TestResultsScreen screen = null; + + var score = TestResources.CreateTestScoreInfo(); + + score.OnlineID = 1234; + score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); + score.Accuracy = accuracy; + score.Rank = rank; + + loadResultsScreen(() => screen = createResultsScreen(score)); + AddUntilStep("wait for loaded", () => screen.IsLoaded); + AddAssert("retry overlay present", () => screen.RetryOverlay != null); + } + [Test] public void TestResultsWithoutPlayer() { @@ -83,8 +107,6 @@ namespace osu.Game.Tests.Visual.Ranking }; var score = TestResources.CreateTestScoreInfo(); - score.OnlineID = 1234; - score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); stack.Push(screen = createResultsScreen(score)); }); @@ -92,28 +114,6 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("retry overlay not present", () => screen.RetryOverlay == null); } - [TestCase(0.2, ScoreRank.D)] - [TestCase(0.5, ScoreRank.D)] - [TestCase(0.75, ScoreRank.C)] - [TestCase(0.85, ScoreRank.B)] - [TestCase(0.925, ScoreRank.A)] - [TestCase(0.975, ScoreRank.S)] - [TestCase(0.9999, ScoreRank.S)] - [TestCase(1, ScoreRank.X)] - public void TestResultsWithPlayer(double accuracy, ScoreRank rank) - { - TestResultsScreen screen = null; - - var score = TestResources.CreateTestScoreInfo(); - - score.Accuracy = accuracy; - score.Rank = rank; - - loadResultsScreen(() => screen = createResultsScreen(score)); - AddUntilStep("wait for loaded", () => screen.IsLoaded); - AddAssert("retry overlay present", () => screen.RetryOverlay != null); - } - [Test] public void TestResultsForUnranked() { @@ -332,13 +332,14 @@ namespace osu.Game.Tests.Visual.Ranking } } - private partial class TestResultsScreen : ResultsScreen + private partial class TestResultsScreen : SoloResultsScreen { public HotkeyRetryOverlay RetryOverlay; public TestResultsScreen(ScoreInfo score) : base(score, true) { + ShowUserStatistics = true; } protected override void LoadComplete() From 0173543dcc2f2d639ba8ab74c3976c6a19d0baf0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 18:17:46 +0900 Subject: [PATCH 0659/2100] Change order and test population in `TestSceneStatisticsPanel` to give better visible results --- .../Visual/Ranking/TestSceneStatisticsPanel.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 67211a3b72..69c9fcb637 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -30,19 +30,19 @@ namespace osu.Game.Tests.Visual.Ranking public partial class TestSceneStatisticsPanel : OsuTestScene { [Test] - public void TestScoreWithTimeStatistics() + public void TestScoreWithPositionStatistics() { var score = TestResources.CreateTestScoreInfo(); - score.HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); + score.HitEvents = createPositionDistributedHitEvents(); loadPanel(score); } [Test] - public void TestScoreWithPositionStatistics() + public void TestScoreWithTimeStatistics() { var score = TestResources.CreateTestScoreInfo(); - score.HitEvents = createPositionDistributedHitEvents(); + score.HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); loadPanel(score); } @@ -89,18 +89,19 @@ namespace osu.Game.Tests.Visual.Ranking private static List createPositionDistributedHitEvents() { - var hitEvents = new List(); + var hitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); + // Use constant seed for reproducibility var random = new Random(0); - for (int i = 0; i < 500; i++) + for (int i = 0; i < hitEvents.Count; i++) { double angle = random.NextDouble() * 2 * Math.PI; double radius = random.NextDouble() * 0.5f * OsuHitObject.OBJECT_RADIUS; var position = new Vector2((float)(radius * Math.Cos(angle)), (float)(radius * Math.Sin(angle))); - hitEvents.Add(new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), position)); + hitEvents[i] = hitEvents[i].With(position); } return hitEvents; From 6e9785ec589fe7f586d213b4cb17242dbb6360eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 01:06:26 +0900 Subject: [PATCH 0660/2100] Add full statistics score to `TestSceneResultsScreen` --- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 5 ++++- osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index c5b61c1a90..4eba60de3c 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -82,7 +82,10 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both }; - stack.Push(screen = createResultsScreen()); + var score = TestResources.CreateTestScoreInfo(); + score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); + + stack.Push(screen = createResultsScreen(score)); }); AddUntilStep("wait for loaded", () => screen.IsLoaded); AddAssert("retry overlay not present", () => screen.RetryOverlay == null); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 69c9fcb637..e02782678f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking public void TestScoreWithPositionStatistics() { var score = TestResources.CreateTestScoreInfo(); - score.HitEvents = createPositionDistributedHitEvents(); + score.HitEvents = CreatePositionDistributedHitEvents(); loadPanel(score); } @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Ranking }; }); - private static List createPositionDistributedHitEvents() + public static List CreatePositionDistributedHitEvents() { var hitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); From b43949300bdee77effee0e6c5bfb5dc8daf012c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 19:06:48 +0900 Subject: [PATCH 0661/2100] Show online stats on `TestSceneStatisticsPanel` Fix test regression --- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 1 + osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 4eba60de3c..937bbe1448 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -83,6 +83,7 @@ namespace osu.Game.Tests.Visual.Ranking }; var score = TestResources.CreateTestScoreInfo(); + score.OnlineID = 1234; score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); stack.Push(screen = createResultsScreen(score)); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index e02782678f..e0d743b046 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Online.Solo; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; @@ -23,6 +24,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Tests.Resources; +using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -33,6 +35,7 @@ namespace osu.Game.Tests.Visual.Ranking public void TestScoreWithPositionStatistics() { var score = TestResources.CreateTestScoreInfo(); + score.OnlineID = 1234; score.HitEvents = CreatePositionDistributedHitEvents(); loadPanel(score); @@ -79,11 +82,12 @@ namespace osu.Game.Tests.Visual.Ranking private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { - Child = new StatisticsPanel + Child = new SoloStatisticsPanel(score) { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, - Score = { Value = score } + Score = { Value = score }, + StatisticsUpdate = { Value = new SoloStatisticsUpdate(score, new UserStatistics(), new UserStatistics()) } }; }); From 48670308a501205d518ba1cb086e64cc4e1dfcdb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 01:29:58 +0900 Subject: [PATCH 0662/2100] Add actual statistics changes to better visualise layout --- .../Ranking/TestSceneStatisticsPanel.cs | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index e0d743b046..93005271a9 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -87,7 +87,44 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, Score = { Value = score }, - StatisticsUpdate = { Value = new SoloStatisticsUpdate(score, new UserStatistics(), new UserStatistics()) } + StatisticsUpdate = + { + Value = new SoloStatisticsUpdate(score, new UserStatistics + { + Level = new UserStatistics.LevelInfo + { + Current = 5, + Progress = 20, + }, + GlobalRank = 38000, + CountryRank = 12006, + PP = 2134, + RankedScore = 21123849, + Accuracy = 0.985, + PlayCount = 13375, + PlayTime = 354490, + TotalScore = 128749597, + TotalHits = 0, + MaxCombo = 1233, + }, new UserStatistics + { + Level = new UserStatistics.LevelInfo + { + Current = 5, + Progress = 30, + }, + GlobalRank = 36000, + CountryRank = 12000, + PP = (decimal)2134.5, + RankedScore = 23897015, + Accuracy = 0.984, + PlayCount = 13376, + PlayTime = 35789, + TotalScore = 132218497, + TotalHits = 0, + MaxCombo = 1233, + }) + } }; }); From 8c4831e09f6c8acd1cd0919b2990dbcba0bfb45a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 01:24:09 +0900 Subject: [PATCH 0663/2100] Reorder tests for testability --- .../Visual/Ranking/TestSceneResultsScreen.cs | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 937bbe1448..1c904a936f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -69,6 +69,30 @@ namespace osu.Game.Tests.Visual.Ranking })); } + [TestCase(1, ScoreRank.X)] + [TestCase(0.9999, ScoreRank.S)] + [TestCase(0.975, ScoreRank.S)] + [TestCase(0.925, ScoreRank.A)] + [TestCase(0.85, ScoreRank.B)] + [TestCase(0.75, ScoreRank.C)] + [TestCase(0.5, ScoreRank.D)] + [TestCase(0.2, ScoreRank.D)] + public void TestResultsWithPlayer(double accuracy, ScoreRank rank) + { + TestResultsScreen screen = null; + + var score = TestResources.CreateTestScoreInfo(); + + score.OnlineID = 1234; + score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); + score.Accuracy = accuracy; + score.Rank = rank; + + loadResultsScreen(() => screen = createResultsScreen(score)); + AddUntilStep("wait for loaded", () => screen.IsLoaded); + AddAssert("retry overlay present", () => screen.RetryOverlay != null); + } + [Test] public void TestResultsWithoutPlayer() { @@ -83,8 +107,6 @@ namespace osu.Game.Tests.Visual.Ranking }; var score = TestResources.CreateTestScoreInfo(); - score.OnlineID = 1234; - score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); stack.Push(screen = createResultsScreen(score)); }); @@ -92,28 +114,6 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("retry overlay not present", () => screen.RetryOverlay == null); } - [TestCase(0.2, ScoreRank.D)] - [TestCase(0.5, ScoreRank.D)] - [TestCase(0.75, ScoreRank.C)] - [TestCase(0.85, ScoreRank.B)] - [TestCase(0.925, ScoreRank.A)] - [TestCase(0.975, ScoreRank.S)] - [TestCase(0.9999, ScoreRank.S)] - [TestCase(1, ScoreRank.X)] - public void TestResultsWithPlayer(double accuracy, ScoreRank rank) - { - TestResultsScreen screen = null; - - var score = TestResources.CreateTestScoreInfo(); - - score.Accuracy = accuracy; - score.Rank = rank; - - loadResultsScreen(() => screen = createResultsScreen(score)); - AddUntilStep("wait for loaded", () => screen.IsLoaded); - AddAssert("retry overlay present", () => screen.RetryOverlay != null); - } - [Test] public void TestResultsForUnranked() { @@ -332,13 +332,14 @@ namespace osu.Game.Tests.Visual.Ranking } } - private partial class TestResultsScreen : ResultsScreen + private partial class TestResultsScreen : SoloResultsScreen { public HotkeyRetryOverlay RetryOverlay; public TestResultsScreen(ScoreInfo score) : base(score, true) { + ShowUserStatistics = true; } protected override void LoadComplete() From 98bf15182efada7ceb1b9d9fa2b1ca9a6bef02f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 18:17:57 +0900 Subject: [PATCH 0664/2100] Remove more `GridContainer` nonsense --- .../Ranking/Statistics/StatisticContainer.cs | 29 ++++++------------- .../Ranking/Statistics/StatisticsPanel.cs | 24 ++++----------- 2 files changed, 15 insertions(+), 38 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index 9191ee6f52..f3308e9931 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -26,31 +26,20 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - InternalChild = new GridContainer + InternalChild = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Content = new[] + Children = new[] { - new[] + createHeader(item), + new Container { - createHeader(item) - }, - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = 15 }, - Child = item.CreateContent() - } - }, - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 15 }, + Child = item.CreateContent() + } } }; } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 8b059efaf4..40d2d29902 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -124,7 +124,7 @@ namespace osu.Game.Screens.Ranking.Statistics } else { - FillFlowContainer rows; + FillFlowContainer flow; container = new OsuScrollContainer(Direction.Vertical) { RelativeSizeAxes = Axes.Both, @@ -133,11 +133,12 @@ namespace osu.Game.Screens.Ranking.Statistics Alpha = 0, Children = new[] { - rows = new FillFlowContainer + flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(30, 15) + Spacing = new Vector2(30, 15), + Direction = FillDirection.Full, } } }; @@ -146,35 +147,22 @@ namespace osu.Game.Screens.Ranking.Statistics foreach (var item in statisticItems) { - var columnContent = new List(); - if (!hitEventsAvailable && item.RequiresHitEvents) { anyRequiredHitEvents = true; continue; } - columnContent.Add(new StatisticContainer(item) + flow.Add(new StatisticContainer(item) { Anchor = Anchor.Centre, Origin = Anchor.Centre, }); - - rows.Add(new GridContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Content = new[] { columnContent.ToArray() }, - ColumnDimensions = new[] { new Dimension() }, - RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } - }); } if (anyRequiredHitEvents) { - rows.Add(new FillFlowContainer + flow.Add(new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, From 7637a9e60394c79d3e1f1155b896ef75f8793cd9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 18:16:09 +0900 Subject: [PATCH 0665/2100] Adjust metrics of `OverallRanking` --- .../Ranking/Statistics/User/OverallRanking.cs | 2 +- .../Statistics/User/RankingChangeRow.cs | 25 +++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs index 447f206128..1d39023223 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Spacing = new Vector2(10), + Spacing = new Vector2(5), Children = new Drawable[] { new GlobalRankChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, diff --git a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs index 5348b4a522..534e1e58cd 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics; @@ -46,7 +47,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User new OsuSpriteText { Text = Label, - Font = OsuFont.Default.With(size: 18) + Font = OsuFont.Default.With(size: 14) }, new FillFlowContainer { @@ -65,17 +66,31 @@ namespace osu.Game.Screens.Ranking.Statistics.User Spacing = new Vector2(5), Children = new Drawable[] { - changeIcon = new SpriteIcon + new Container { + Size = new Vector2(14), Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Size = new Vector2(18) + Children = new Drawable[] + { + new Circle + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray1 + }, + changeIcon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(10), + }, + } }, currentValueText = new OsuSpriteText { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Font = OsuFont.Default.With(size: 18, weight: FontWeight.Bold) + Font = OsuFont.Default.With(size: 14, weight: FontWeight.Bold) }, } }, @@ -123,7 +138,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User } else { - comparisonColour = colours.Orange1; + comparisonColour = colours.Gray4; icon = FontAwesome.Solid.Minus; } From f223fd7c3bf5bd19b36b8abe613e6fd549576412 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 18:16:15 +0900 Subject: [PATCH 0666/2100] Adjust metrics of `PerformanceBreakdown` --- .../Ranking/Statistics/PerformanceBreakdownChart.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs index 10cb77fa91..6979f0ad94 100644 --- a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs +++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs @@ -97,7 +97,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14), Text = "Achieved PP", Colour = Color4Extensions.FromHex("#66FFCC") }, @@ -105,7 +105,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreRight, Anchor = Anchor.CentreRight, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 18), + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 14), Colour = Color4Extensions.FromHex("#66FFCC") } }, @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14), Text = "Maximum", Colour = OsuColour.Gray(0.7f) }, @@ -123,7 +123,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14), Colour = OsuColour.Gray(0.7f) } } @@ -208,7 +208,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14), Text = attribute.DisplayName, Colour = Colour4.White }, @@ -233,7 +233,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreRight, Anchor = Anchor.CentreRight, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 14), Text = percentage.ToLocalisableString("0%"), Colour = Colour4.White } From 4f089eb5a5d7d471d2d218a68df32c996c58f45a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 18:16:55 +0900 Subject: [PATCH 0667/2100] Adjust metrics of `AccuracyHeatmap` --- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 5d2f6a14c7..6564a086fe 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Scoring; @@ -120,18 +121,22 @@ namespace osu.Game.Rulesets.Osu.Statistics new OsuSpriteText { Text = "Overshoot", + Font = OsuFont.GetFont(size: 12), Anchor = Anchor.Centre, - Origin = Anchor.BottomCentre, - Padding = new MarginPadding(3), + Origin = Anchor.BottomLeft, + Padding = new MarginPadding(2), + Rotation = -rotation, RelativePositionAxes = Axes.Both, Y = -(inner_portion + line_extension) / 2, }, new OsuSpriteText { Text = "Undershoot", + Font = OsuFont.GetFont(size: 12), Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - Padding = new MarginPadding(3), + Origin = Anchor.TopRight, + Rotation = -rotation, + Padding = new MarginPadding(2), RelativePositionAxes = Axes.Both, Y = (inner_portion + line_extension) / 2, }, From d54cf639839f847e60fcc56202f322d0f59d0ad6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 18:29:20 +0900 Subject: [PATCH 0668/2100] Centralise font size specification for statistic items (and reduce slightly) --- .../Statistics/HitEventTimingDistributionGraph.cs | 8 ++++---- .../Ranking/Statistics/PerformanceBreakdownChart.cs | 12 ++++++------ .../Ranking/Statistics/SimpleStatisticItem.cs | 4 ++-- .../Screens/Ranking/Statistics/StatisticContainer.cs | 2 +- osu.Game/Screens/Ranking/Statistics/StatisticItem.cs | 5 +++++ .../Ranking/Statistics/User/RankingChangeRow.cs | 4 ++-- 6 files changed, 20 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index e8d2eefd81..fb9a78fb54 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -154,7 +154,7 @@ namespace osu.Game.Screens.Ranking.Statistics axisFlow = new Container { RelativeSizeAxes = Axes.X, - Height = axis_font_size, + Height = StatisticItem.FONT_SIZE, } }, }, @@ -174,7 +174,7 @@ namespace osu.Game.Screens.Ranking.Statistics Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = "0", - Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold) }); for (int i = 1; i <= axis_points; i++) @@ -191,7 +191,7 @@ namespace osu.Game.Screens.Ranking.Statistics X = -position / 2, Alpha = alpha, Text = axisValue.ToString("-0"), - Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold) }); axisFlow.Add(new OsuSpriteText @@ -202,7 +202,7 @@ namespace osu.Game.Screens.Ranking.Statistics X = position / 2, Alpha = alpha, Text = axisValue.ToString("+0"), - Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold) }); } } diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs index 6979f0ad94..ee0ce6183d 100644 --- a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs +++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs @@ -97,7 +97,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: StatisticItem.FONT_SIZE), Text = "Achieved PP", Colour = Color4Extensions.FromHex("#66FFCC") }, @@ -105,7 +105,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreRight, Anchor = Anchor.CentreRight, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 14), + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: StatisticItem.FONT_SIZE), Colour = Color4Extensions.FromHex("#66FFCC") } }, @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: StatisticItem.FONT_SIZE), Text = "Maximum", Colour = OsuColour.Gray(0.7f) }, @@ -123,7 +123,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: StatisticItem.FONT_SIZE), Colour = OsuColour.Gray(0.7f) } } @@ -208,7 +208,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: StatisticItem.FONT_SIZE), Text = attribute.DisplayName, Colour = Colour4.White }, @@ -233,7 +233,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreRight, Anchor = Anchor.CentreRight, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 14), + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: StatisticItem.FONT_SIZE), Text = percentage.ToLocalisableString("0%"), Colour = Colour4.White } diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs index 99f4e1e342..23ccc3d0b7 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs @@ -44,13 +44,13 @@ namespace osu.Game.Screens.Ranking.Statistics Text = Name, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 14) + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE) }, value = new OsuSpriteText { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold) + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.Bold) } }); } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index f3308e9931..c3cc443adb 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Ranking.Statistics Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Text = item.Name, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold), } } }; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index fd7a0ddb4f..cf95886905 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -12,6 +12,11 @@ namespace osu.Game.Screens.Ranking.Statistics /// public class StatisticItem { + /// + /// The recommended font size to use in statistic items to make sure they match others. + /// + public const float FONT_SIZE = 13; + /// /// The name of this item. /// diff --git a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs index 534e1e58cd..a58e028baa 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User new OsuSpriteText { Text = Label, - Font = OsuFont.Default.With(size: 14) + Font = OsuFont.Default.With(size: StatisticItem.FONT_SIZE) }, new FillFlowContainer { @@ -90,7 +90,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Font = OsuFont.Default.With(size: 14, weight: FontWeight.Bold) + Font = OsuFont.Default.With(size: StatisticItem.FONT_SIZE, weight: FontWeight.Bold) }, } }, From 6edaf4f230c4b0ab82f6ca8dbab0e3774cdec1a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 17:52:08 +0900 Subject: [PATCH 0669/2100] Ensure `PerformanceBreakdown` pieces cannot be null --- .../Rulesets/Difficulty/PerformanceBreakdown.cs | 13 +++++++++++-- .../Difficulty/PerformanceBreakdownCalculator.cs | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs index bd971db476..6e41855ca3 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Rulesets.Difficulty { /// @@ -19,5 +17,16 @@ namespace osu.Game.Rulesets.Difficulty /// Performance of a perfect play for comparison. /// public PerformanceAttributes PerfectPerformance { get; set; } + + /// + /// Create a new performance breakdown. + /// + /// Actual gameplay performance. + /// Performance of a perfect play for comparison. + public PerformanceBreakdown(PerformanceAttributes performance, PerformanceAttributes perfectPerformance) + { + Performance = performance; + PerfectPerformance = perfectPerformance; + } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 8b59500f43..ad9257d4f3 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Difficulty getPerfectPerformance(score, cancellationToken) ).ConfigureAwait(false); - return new PerformanceBreakdown { Performance = performanceArray[0], PerfectPerformance = performanceArray[1] }; + return new PerformanceBreakdown(performanceArray[0] ?? new PerformanceAttributes(), performanceArray[1] ?? new PerformanceAttributes()); } [ItemCanBeNull] From 947b40149f3e7581f15d40d6daeccf772bf8ac61 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 18:30:05 +0900 Subject: [PATCH 0670/2100] Adjust metrics of `SimpleStatisticTable` --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index bd6ab4086b..620009e182 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -412,7 +412,7 @@ namespace osu.Game.Rulesets.Mania RelativeSizeAxes = Axes.X, Height = 250 }, true), - new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem(string.Empty, () => new SimpleStatisticTable(2, new SimpleStatisticItem[] { new AverageHitError(score.HitEvents), new UnstableRate(score.HitEvents) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 036d13c5aa..3b24cc5997 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -319,7 +319,7 @@ namespace osu.Game.Rulesets.Osu RelativeSizeAxes = Axes.X, Height = 250 }, true), - new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem(string.Empty, () => new SimpleStatisticTable(2, new SimpleStatisticItem[] { new AverageHitError(timedHitEvents), new UnstableRate(timedHitEvents) diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs index d68df4558a..4eb4979724 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs @@ -103,7 +103,6 @@ namespace osu.Game.Screens.Ranking.Statistics public Spacer() { RelativeSizeAxes = Axes.Both; - Padding = new MarginPadding { Vertical = 4 }; InternalChild = new CircularContainer { From 0881f4772cb730855cb8ebc5ec8c27327bd8dc60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 18:31:45 +0900 Subject: [PATCH 0671/2100] Adjust metrics of `HitEventTimingDistributionGraph` --- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index fb9a78fb54..1260ec2339 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -131,14 +131,11 @@ namespace osu.Game.Screens.Ranking.Statistics Container axisFlow; - const float axis_font_size = 12; + Padding = new MarginPadding { Horizontal = 5 }; InternalChild = new GridContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Width = 0.8f, Content = new[] { new Drawable[] From 1a7b00ec152c15f467b05e1995bbfd32c7267403 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 17:52:08 +0900 Subject: [PATCH 0672/2100] Ensure `PerformanceBreakdown` pieces cannot be null --- .../Rulesets/Difficulty/PerformanceBreakdown.cs | 13 +++++++++++-- .../Difficulty/PerformanceBreakdownCalculator.cs | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs index bd971db476..6e41855ca3 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Rulesets.Difficulty { /// @@ -19,5 +17,16 @@ namespace osu.Game.Rulesets.Difficulty /// Performance of a perfect play for comparison. /// public PerformanceAttributes PerfectPerformance { get; set; } + + /// + /// Create a new performance breakdown. + /// + /// Actual gameplay performance. + /// Performance of a perfect play for comparison. + public PerformanceBreakdown(PerformanceAttributes performance, PerformanceAttributes perfectPerformance) + { + Performance = performance; + PerfectPerformance = perfectPerformance; + } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 8b59500f43..ad9257d4f3 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Difficulty getPerfectPerformance(score, cancellationToken) ).ConfigureAwait(false); - return new PerformanceBreakdown { Performance = performanceArray[0], PerfectPerformance = performanceArray[1] }; + return new PerformanceBreakdown(performanceArray[0] ?? new PerformanceAttributes(), performanceArray[1] ?? new PerformanceAttributes()); } [ItemCanBeNull] From b333945cde705b12b8de3b32838cf532dc3a9a6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 01:58:38 +0900 Subject: [PATCH 0673/2100] Change `OverallRanking` to use a two-column layout similar to statistics table --- .../Statistics/SimpleStatisticTable.cs | 2 +- .../Ranking/Statistics/User/OverallRanking.cs | 49 ++++++++++++++----- .../Statistics/User/RankingChangeRow.cs | 5 +- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs index 4eb4979724..ed31bc8643 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs @@ -98,7 +98,7 @@ namespace osu.Game.Screens.Ranking.Statistics Direction = FillDirection.Vertical }; - private partial class Spacer : CompositeDrawable + public partial class Spacer : CompositeDrawable { public Spacer() { diff --git a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs index 1d39023223..d08a654e99 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Solo; -using osuTK; namespace osu.Game.Screens.Ranking.Statistics.User { @@ -18,7 +17,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User public Bindable StatisticsUpdate { get; } = new Bindable(); private LoadingLayer loadingLayer = null!; - private FillFlowContainer content = null!; + private GridContainer content = null!; [BackgroundDependencyLoader] private void load() @@ -33,21 +32,47 @@ namespace osu.Game.Screens.Ranking.Statistics.User { RelativeSizeAxes = Axes.Both, }, - content = new FillFlowContainer + content = new GridContainer { AlwaysPresent = true, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(5), - Children = new Drawable[] + ColumnDimensions = new[] { - new GlobalRankChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, - new AccuracyChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, - new MaximumComboChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, - new RankedScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, - new TotalScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, - new PerformancePointsChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } } + new Dimension(), + new Dimension(GridSizeMode.Absolute, 30), + new Dimension(), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + new GlobalRankChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new SimpleStatisticTable.Spacer(), + new PerformancePointsChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + }, + new Drawable[] { }, + new Drawable[] + { + new MaximumComboChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new SimpleStatisticTable.Spacer(), + new AccuracyChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + }, + new Drawable[] { }, + new Drawable[] + { + new RankedScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new SimpleStatisticTable.Spacer(), + new TotalScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + } } } }; diff --git a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs index a58e028baa..906bf8d5ca 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs @@ -54,7 +54,8 @@ namespace osu.Game.Screens.Ranking.Statistics.User Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, + AutoSizeAxes = Axes.X, + Height = StatisticItem.FONT_SIZE * 2, Children = new Drawable[] { new FillFlowContainer @@ -98,7 +99,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Font = OsuFont.Default.With(weight: FontWeight.Bold) + Font = OsuFont.Default.With(size: StatisticItem.FONT_SIZE, weight: FontWeight.Bold) } } } From 2073810e958b7376d4797de1dc1e5d79213be7d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 18:48:24 +0900 Subject: [PATCH 0674/2100] Add performance breakdown chart for osu!catch --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index e51e5cc5db..bd6b857fe8 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -26,6 +26,8 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch @@ -218,5 +220,17 @@ namespace osu.Game.Rulesets.Catch public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this); public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier(); + + public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) + { + return new[] + { + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }), + }; + } } } From 9ad63bae374262f1a1158dcc1975eecda1595e39 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 18:50:52 +0900 Subject: [PATCH 0675/2100] Add missing heading for statistics section in results screen --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index bd6ab4086b..e46c85ab29 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -412,7 +412,7 @@ namespace osu.Game.Rulesets.Mania RelativeSizeAxes = Axes.X, Height = 250 }, true), - new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem("Statistics", () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new AverageHitError(score.HitEvents), new UnstableRate(score.HitEvents) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 036d13c5aa..c00bab8ed7 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -319,7 +319,7 @@ namespace osu.Game.Rulesets.Osu RelativeSizeAxes = Axes.X, Height = 250 }, true), - new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem("Statistics", () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new AverageHitError(timedHitEvents), new UnstableRate(timedHitEvents) diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index de3fa1750f..feb8b0c2c2 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -256,7 +256,7 @@ namespace osu.Game.Rulesets.Taiko RelativeSizeAxes = Axes.X, Height = 250 }, true), - new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem("Statistics", () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new AverageHitError(timedHitEvents), new UnstableRate(timedHitEvents) From e34839c8918ef5fbda6fe2b547c4963feceed223 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 18:55:54 +0900 Subject: [PATCH 0676/2100] Rename `StatisticContainer` to `StatisticItemContainer` and add a background --- ...Container.cs => StatisticItemContainer.cs} | 43 ++++++++++++++----- .../Ranking/Statistics/StatisticsPanel.cs | 2 +- 2 files changed, 34 insertions(+), 11 deletions(-) rename osu.Game/Screens/Ranking/Statistics/{StatisticContainer.cs => StatisticItemContainer.cs} (62%) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItemContainer.cs similarity index 62% rename from osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs rename to osu.Game/Screens/Ranking/Statistics/StatisticItemContainer.cs index c3cc443adb..6e18ae1fe4 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItemContainer.cs @@ -3,6 +3,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; @@ -15,31 +16,53 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// Wraps a to add a header and suitable layout for use in . /// - internal partial class StatisticContainer : CompositeDrawable + internal partial class StatisticItemContainer : CompositeDrawable { /// - /// Creates a new . + /// Creates a new . /// /// The to display. - public StatisticContainer(StatisticItem item) + public StatisticItemContainer(StatisticItem item) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - InternalChild = new FillFlowContainer + Padding = new MarginPadding(5); + + InternalChild = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Children = new[] + Masking = true, + CornerRadius = 6, + Children = new Drawable[] { - createHeader(item), + new Box + { + Colour = ColourInfo.GradientVertical( + OsuColour.Gray(0.25f), + OsuColour.Gray(0.18f) + ), + Alpha = 0.95f, + RelativeSizeAxes = Axes.Both, + }, new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = 15 }, - Child = item.CreateContent() - } + Padding = new MarginPadding(5), + Children = new[] + { + createHeader(item), + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(10) { Top = 30 }, + Child = item.CreateContent() + } + } + }, } }; } @@ -52,7 +75,7 @@ namespace osu.Game.Screens.Ranking.Statistics return new FillFlowContainer { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + Height = 20, Direction = FillDirection.Horizontal, Spacing = new Vector2(5, 0), Children = new Drawable[] diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 40d2d29902..8ab774f76d 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -153,7 +153,7 @@ namespace osu.Game.Screens.Ranking.Statistics continue; } - flow.Add(new StatisticContainer(item) + flow.Add(new StatisticItemContainer(item) { Anchor = Anchor.Centre, Origin = Anchor.Centre, From 2c27b17c85a14a39bc4dbdc0e5297945d6e0e838 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 19:00:03 +0900 Subject: [PATCH 0677/2100] Disable masking on inner scroll container --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 8ab774f76d..9357d1f385 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -130,6 +130,8 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, + Masking = false, + ScrollbarOverlapsContent = false, Alpha = 0, Children = new[] { From cde8d8e7f1fccc221527209216c24a73612e7d71 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 13 Jul 2023 14:33:21 +0200 Subject: [PATCH 0678/2100] Optimize realm transactions for editor Save action --- osu.Game/Beatmaps/BeatmapManager.cs | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 54c243e842..265149e378 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -431,8 +431,9 @@ namespace osu.Game.Beatmaps beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; beatmapInfo.ResetOnlineInfo(); - using (var stream = new MemoryStream()) + Realm.Write(r => { + using var stream = new MemoryStream(); using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); @@ -458,23 +459,20 @@ namespace osu.Game.Beatmaps updateHashAndMarkDirty(setInfo); - Realm.Write(r => - { - var liveBeatmapSet = r.Find(setInfo.ID)!; + var liveBeatmapSet = r.Find(setInfo.ID)!; - setInfo.CopyChangesToRealm(liveBeatmapSet); + setInfo.CopyChangesToRealm(liveBeatmapSet); - if (transferCollections) - beatmapInfo.TransferCollectionReferences(r, oldMd5Hash); + if (transferCollections) + beatmapInfo.TransferCollectionReferences(r, oldMd5Hash); - liveBeatmapSet.Beatmaps.Single(b => b.ID == beatmapInfo.ID) - .UpdateLocalScores(r); + liveBeatmapSet.Beatmaps.Single(b => b.ID == beatmapInfo.ID) + .UpdateLocalScores(r); - // do not look up metadata. - // this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst. - ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None); - }); - } + // do not look up metadata. + // this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst. + ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None); + }); Debug.Assert(beatmapInfo.BeatmapSet != null); From 20e4e2581a530dc020d0b8c3252ba961110ee04e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 22:12:55 +0900 Subject: [PATCH 0679/2100] Change `IBeatSyncProvider.Clock` to always be non-null --- .../Visual/UserInterface/TestSceneBeatSyncedContainer.cs | 5 ----- osu.Game/Beatmaps/BeatSyncProviderExtensions.cs | 7 +------ osu.Game/Beatmaps/IBeatSyncProvider.cs | 2 +- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 5 +---- osu.Game/Rulesets/Mods/MetronomeBeat.cs | 2 +- osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs | 4 ++-- osu.Game/Screens/Edit/Timing/TapButton.cs | 2 +- osu.Game/Screens/Menu/LogoVisualisation.cs | 3 +-- .../Storyboards/Drawables/DrawableStoryboardAnimation.cs | 2 +- 9 files changed, 9 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 5d97714ab5..c723610614 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Audio.Track; @@ -283,8 +282,6 @@ namespace osu.Game.Tests.Visual.UserInterface if (ReferenceEquals(timingPoints[^1], current)) { - Debug.Assert(BeatSyncSource.Clock != null); - return (int)Math.Ceiling((BeatSyncSource.Clock.CurrentTime - current.Time) / current.BeatLength); } @@ -295,8 +292,6 @@ namespace osu.Game.Tests.Visual.UserInterface { base.Update(); - Debug.Assert(BeatSyncSource.Clock != null); - timeUntilNextBeat.Value = TimeUntilNextBeat; timeSinceLastBeat.Value = TimeSinceLastBeat; currentTime.Value = BeatSyncSource.Clock.CurrentTime; diff --git a/osu.Game/Beatmaps/BeatSyncProviderExtensions.cs b/osu.Game/Beatmaps/BeatSyncProviderExtensions.cs index 767aa5df73..e2b805bf0d 100644 --- a/osu.Game/Beatmaps/BeatSyncProviderExtensions.cs +++ b/osu.Game/Beatmaps/BeatSyncProviderExtensions.cs @@ -5,14 +5,9 @@ namespace osu.Game.Beatmaps { public static class BeatSyncProviderExtensions { - /// - /// Check whether beat sync is currently available. - /// - public static bool CheckBeatSyncAvailable(this IBeatSyncProvider provider) => provider.Clock != null; - /// /// Whether the beat sync provider is currently in a kiai section. Should make everything more epic. /// - public static bool CheckIsKiaiTime(this IBeatSyncProvider provider) => provider.Clock != null && provider.ControlPoints?.EffectPointAt(provider.Clock.CurrentTime).KiaiMode == true; + public static bool CheckIsKiaiTime(this IBeatSyncProvider provider) => provider.ControlPoints?.EffectPointAt(provider.Clock.CurrentTime).KiaiMode == true; } } diff --git a/osu.Game/Beatmaps/IBeatSyncProvider.cs b/osu.Game/Beatmaps/IBeatSyncProvider.cs index 9ee19e720d..776552cfa5 100644 --- a/osu.Game/Beatmaps/IBeatSyncProvider.cs +++ b/osu.Game/Beatmaps/IBeatSyncProvider.cs @@ -24,6 +24,6 @@ namespace osu.Game.Beatmaps /// /// Access a clock currently responsible for providing beat sync. If null, no current provider is available. /// - IClock? Clock { get; } + IClock Clock { get; } } } diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 42b30f9d18..f911311a09 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Containers; @@ -86,14 +85,12 @@ namespace osu.Game.Graphics.Containers TimingControlPoint timingPoint; EffectControlPoint effectPoint; - IsBeatSyncedWithTrack = BeatSyncSource.CheckBeatSyncAvailable() && BeatSyncSource.Clock?.IsRunning == true; + IsBeatSyncedWithTrack = BeatSyncSource.Clock.IsRunning; double currentTrackTime; if (IsBeatSyncedWithTrack) { - Debug.Assert(BeatSyncSource.Clock != null); - currentTrackTime = BeatSyncSource.Clock.CurrentTime + EarlyActivationMilliseconds; timingPoint = BeatSyncSource.ControlPoints?.TimingPointAt(currentTrackTime) ?? TimingControlPoint.DEFAULT; diff --git a/osu.Game/Rulesets/Mods/MetronomeBeat.cs b/osu.Game/Rulesets/Mods/MetronomeBeat.cs index 265970ea46..5615362d1a 100644 --- a/osu.Game/Rulesets/Mods/MetronomeBeat.cs +++ b/osu.Game/Rulesets/Mods/MetronomeBeat.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mods int timeSignature = timingPoint.TimeSignature.Numerator; // play metronome from one measure before the first object. - if (BeatSyncSource.Clock?.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature) + if (BeatSyncSource.Clock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature) return; sample.Frequency.Value = beatIndex % timeSignature == 0 ? 1 : 0.5f; diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index f4a39405a1..9f03281d0c 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -240,7 +240,7 @@ namespace osu.Game.Screens.Edit.Timing { base.Update(); - if (BeatSyncSource.ControlPoints == null || BeatSyncSource.Clock == null) + if (BeatSyncSource.ControlPoints == null) return; metronomeClock.Rate = IsBeatSyncedWithTrack ? BeatSyncSource.Clock.Rate : 1; @@ -259,7 +259,7 @@ namespace osu.Game.Screens.Edit.Timing this.TransformBindableTo(interpolatedBpm, (int)Math.Round(timingPoint.BPM), 600, Easing.OutQuint); } - if (BeatSyncSource.Clock?.IsRunning != true && isSwinging) + if (!BeatSyncSource.Clock.IsRunning && isSwinging) { swing.ClearTransforms(true); diff --git a/osu.Game/Screens/Edit/Timing/TapButton.cs b/osu.Game/Screens/Edit/Timing/TapButton.cs index f28c6ccf0a..fd60fb1b5b 100644 --- a/osu.Game/Screens/Edit/Timing/TapButton.cs +++ b/osu.Game/Screens/Edit/Timing/TapButton.cs @@ -310,7 +310,7 @@ namespace osu.Game.Screens.Edit.Timing } double averageBeatLength = (tapTimings.Last() - tapTimings.Skip(initial_taps_to_ignore).First()) / (tapTimings.Count - initial_taps_to_ignore - 1); - double clockRate = beatSyncSource?.Clock?.Rate ?? 1; + double clockRate = beatSyncSource?.Clock.Rate ?? 1; double bpm = Math.Round(60000 / averageBeatLength / clockRate); diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 5000a97b3d..fa26cfab46 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -102,8 +102,7 @@ namespace osu.Game.Screens.Menu for (int i = 0; i < temporalAmplitudes.Length; i++) temporalAmplitudes[i] = 0; - if (beatSyncProvider.Clock != null) - addAmplitudesFromSource(beatSyncProvider); + addAmplitudesFromSource(beatSyncProvider); foreach (var source in amplitudeSources) addAmplitudesFromSource(source); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index be77c9a98e..82c01ea6a1 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -128,7 +128,7 @@ namespace osu.Game.Storyboards.Drawables // // In the case of storyboard animations, we want to synchronise with game time perfectly // so let's get a correct time based on gameplay clock and earliest transform. - PlaybackPosition = (beatSyncProvider.Clock?.CurrentTime ?? Clock.CurrentTime) - Animation.EarliestTransformTime; + PlaybackPosition = beatSyncProvider.Clock.CurrentTime - Animation.EarliestTransformTime; } private void skinSourceChanged() From 1698caa078e12a33e808ba5b86c00f3c805e5bb8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 20:31:00 +0900 Subject: [PATCH 0680/2100] Add kiai fountains to main menu --- .../Visual/Menus/TestSceneStarFountain.cs | 52 +++++++++ osu.Game/Screens/Menu/KiaiMenuFountains.cs | 67 ++++++++++++ osu.Game/Screens/Menu/MainMenu.cs | 1 + osu.Game/Screens/Menu/StarFountain.cs | 102 ++++++++++++++++++ 4 files changed, 222 insertions(+) create mode 100644 osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs create mode 100644 osu.Game/Screens/Menu/KiaiMenuFountains.cs create mode 100644 osu.Game/Screens/Menu/StarFountain.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs new file mode 100644 index 0000000000..b12f3e7946 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs @@ -0,0 +1,52 @@ +// 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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Menus +{ + [TestFixture] + public partial class TestSceneStarFountain : OsuTestScene + { + [SetUpSteps] + public void SetUpSteps() + { + AddStep("make fountains", () => + { + Children = new[] + { + new StarFountain + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + X = 200, + }, + new StarFountain + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + X = -200, + }, + }; + }); + } + + [Test] + public void TestPew() + { + AddRepeatStep("activate fountains sometimes", () => + { + foreach (var fountain in Children.OfType()) + { + if (RNG.NextSingle() > 0.8f) + fountain.Shoot(); + } + }, 150); + } + } +} diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs new file mode 100644 index 0000000000..a4d58e398a --- /dev/null +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -0,0 +1,67 @@ +// 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.Audio.Track; +using osu.Framework.Graphics; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Screens.Menu +{ + public partial class KiaiMenuFountains : BeatSyncedContainer + { + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + + Children = new[] + { + new StarFountain + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + X = 250, + }, + new StarFountain + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + X = -250, + }, + }; + } + + private bool isTriggered; + + private double? lastTrigger; + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + if (effectPoint.KiaiMode && !isTriggered) + { + bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - effectPoint.Time) < 500; + if (isNearEffectPoint) + Shoot(); + } + + isTriggered = effectPoint.KiaiMode; + } + + public void Shoot() + { + if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500) + return; + + foreach (var fountain in Children.OfType()) + fountain.Shoot(); + + lastTrigger = Clock.CurrentTime; + } + } +} diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index f34a1954a5..baa34d4914 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -136,6 +136,7 @@ namespace osu.Game.Screens.Menu Origin = Anchor.TopRight, Margin = new MarginPadding { Right = 15, Top = 5 } }, + new KiaiMenuFountains(), holdToExitGameOverlay?.CreateProxy() ?? Empty() }); diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs new file mode 100644 index 0000000000..e8feba4451 --- /dev/null +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -0,0 +1,102 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Utils; +using osuTK; + +namespace osu.Game.Screens.Menu +{ + public partial class StarFountain : CompositeDrawable + { + private DrawablePool starPool = null!; + private Container starContainer = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + starPool = new DrawablePool(192), + starContainer = new Container() + }; + } + + public void Shoot() + { + int dir = RNG.Next(-1, 2); + + for (int i = 0; i < 192; i++) + { + double offset = i * 3; + + starContainer.Add(starPool.Get(s => + { + s.Velocity = new Vector2(dir * 0.6f + RNG.NextSingle(-0.25f, 0.25f), -RNG.NextSingle(2.2f, 2.4f)); + s.Position = new Vector2(RNG.NextSingle(-5, 5), 50); + s.Hide(); + + using (s.BeginDelayedSequence(offset)) + { + s.FadeIn(); + s.ScaleTo(1); + + double duration = RNG.Next(300, 1300); + + s.FadeOutFromOne(duration, Easing.Out); + s.ScaleTo(RNG.NextSingle(1, 2.8f), duration, Easing.Out); + + s.Expire(); + } + })); + } + } + + private partial class Star : PoolableDrawable + { + public Vector2 Velocity = Vector2.Zero; + + private float rotation; + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + AutoSizeAxes = Axes.Both; + Origin = Anchor.Centre; + + InternalChildren = new Drawable[] + { + new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Blending = BlendingParameters.Additive, + Scale = new Vector2(0.5f), + Texture = textures.Get("Menu/fountain-star") + } + }; + + rotation = RNG.NextSingle(-2f, 2f); + } + + protected override void Update() + { + const float gravity = 0.004f; + + base.Update(); + + float elapsed = (float)Time.Elapsed; + + Position += Velocity * elapsed; + Velocity += new Vector2(0, elapsed * gravity); + + Rotation += rotation * elapsed; + } + } + } +} From 53fccaa3bd5afeb5413b3cb1537c8808bb386f5a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 22:30:27 +0900 Subject: [PATCH 0681/2100] Use skinnable sprite --- osu.Game/Screens/Menu/StarFountain.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs index e8feba4451..f891e989bb 100644 --- a/osu.Game/Screens/Menu/StarFountain.cs +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -5,9 +5,8 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Framework.Utils; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Screens.Menu @@ -64,20 +63,18 @@ namespace osu.Game.Screens.Menu private float rotation; [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load() { AutoSizeAxes = Axes.Both; Origin = Anchor.Centre; InternalChildren = new Drawable[] { - new Sprite + new SkinnableSprite("star2") { Anchor = Anchor.Centre, Origin = Anchor.Centre, Blending = BlendingParameters.Additive, - Scale = new Vector2(0.5f), - Texture = textures.Get("Menu/fountain-star") } }; From 8f826a3702640fedf73b94127528ebd69e9c469d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 22:57:04 +0900 Subject: [PATCH 0682/2100] Add skinning support for kiai fountain stars --- osu.Game/Screens/Menu/StarFountain.cs | 2 +- osu.Game/Skinning/LegacySkinTransformer.cs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs index f891e989bb..8aedf3199b 100644 --- a/osu.Game/Screens/Menu/StarFountain.cs +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Menu InternalChildren = new Drawable[] { - new SkinnableSprite("star2") + new SkinnableSprite("Menu/fountain-star") { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index 367e5bae01..97b0f3a9b5 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Audio.Sample; +using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Rulesets.Objects.Legacy; using static osu.Game.Skinning.SkinConfiguration; @@ -23,6 +24,18 @@ namespace osu.Game.Skinning { } + public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) + { + switch (componentName) + { + case "fountain-star": + componentName = "star2"; + break; + } + + return base.GetTexture(componentName, wrapModeS, wrapModeT); + } + public override ISample? GetSample(ISampleInfo sampleInfo) { if (!(sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample)) From 1051982bc59d14f937575812e1efe7d659bf24fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 23:12:29 +0900 Subject: [PATCH 0683/2100] Tidy up code with constants --- osu.Game/Screens/Menu/StarFountain.cs | 45 ++++++++++++++++++--------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs index 8aedf3199b..7fa996e39a 100644 --- a/osu.Game/Screens/Menu/StarFountain.cs +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -28,29 +28,42 @@ namespace osu.Game.Screens.Menu public void Shoot() { - int dir = RNG.Next(-1, 2); + // left centre or right movement. + int direction = RNG.Next(-1, 2); - for (int i = 0; i < 192; i++) + const int total_stars = 192; + + const float x_velocity_from_direction = 0.6f; + const float x_velocity_random_variance = 0.25f; + + const float y_velocity_base = -2.0f; + const float y_velocity_random_variance = 0.25f; + + const float x_spawn_position_variance = 10; + const float y_spawn_position_offset = 50; + + for (int i = 0; i < total_stars; i++) { - double offset = i * 3; + double initialOffset = i * 3; starContainer.Add(starPool.Get(s => { - s.Velocity = new Vector2(dir * 0.6f + RNG.NextSingle(-0.25f, 0.25f), -RNG.NextSingle(2.2f, 2.4f)); - s.Position = new Vector2(RNG.NextSingle(-5, 5), 50); + s.Velocity = new Vector2( + direction * x_velocity_from_direction + getRandomVariance(x_velocity_random_variance), + y_velocity_base + getRandomVariance(y_velocity_random_variance)); + + s.Position = new Vector2(getRandomVariance(x_spawn_position_variance), y_spawn_position_offset); + s.Hide(); - using (s.BeginDelayedSequence(offset)) + using (s.BeginDelayedSequence(initialOffset)) { - s.FadeIn(); - s.ScaleTo(1); - double duration = RNG.Next(300, 1300); - s.FadeOutFromOne(duration, Easing.Out); - s.ScaleTo(RNG.NextSingle(1, 2.8f), duration, Easing.Out); - - s.Expire(); + s.ScaleTo(1) + .ScaleTo(RNG.NextSingle(1, 2.8f), duration, Easing.Out) + .FadeOutFromOne(duration, Easing.Out) + .Expire(); } })); } @@ -78,12 +91,12 @@ namespace osu.Game.Screens.Menu } }; - rotation = RNG.NextSingle(-2f, 2f); + rotation = getRandomVariance(2); } protected override void Update() { - const float gravity = 0.004f; + const float gravity = 0.003f; base.Update(); @@ -95,5 +108,7 @@ namespace osu.Game.Screens.Menu Rotation += rotation * elapsed; } } + + private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance); } } From 4b06b6f34ec98b37bdd68e597a8248ac9c36df89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 18:53:32 +0200 Subject: [PATCH 0684/2100] Crudely fix crashes when switching between `TestResultsWithPlayer` cases In the visual test browser, if two `TestResultsWithPlayer` test cases are ran consecutively, the second would die on `SoloStatisticsWatcher` seeing duplicated online score IDs. This surfaced after 6ef39b87fed8b44b5f9219da12d639ae5e4f422b, which changed `TestResultsScreen` to inherit `SoloResultsScreen` rather than `ResultsScreen`. This is probably _not_ a very good fix, but I'm trying to be pragmatic for now. `SoloStatisticsWatcher` should probably not live in `OsuGameBase`. --- .../Visual/Ranking/TestSceneResultsScreen.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 1c904a936f..146482e6fb 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -69,6 +69,8 @@ namespace osu.Game.Tests.Visual.Ranking })); } + private int onlineScoreID = 1; + [TestCase(1, ScoreRank.X)] [TestCase(0.9999, ScoreRank.S)] [TestCase(0.975, ScoreRank.S)] @@ -81,14 +83,17 @@ namespace osu.Game.Tests.Visual.Ranking { TestResultsScreen screen = null; - var score = TestResources.CreateTestScoreInfo(); + loadResultsScreen(() => + { + var score = TestResources.CreateTestScoreInfo(); - score.OnlineID = 1234; - score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); - score.Accuracy = accuracy; - score.Rank = rank; + score.OnlineID = onlineScoreID++; + score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); + score.Accuracy = accuracy; + score.Rank = rank; - loadResultsScreen(() => screen = createResultsScreen(score)); + return screen = createResultsScreen(score); + }); AddUntilStep("wait for loaded", () => screen.IsLoaded); AddAssert("retry overlay present", () => screen.RetryOverlay != null); } From 20bde40ce21b41d5a19add9e5a28e472f1460919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 20:00:25 +0200 Subject: [PATCH 0685/2100] Fix differing anchor specs on statistics panel flow items --- 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 9357d1f385..19bd0c4393 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -157,8 +157,8 @@ namespace osu.Game.Screens.Ranking.Statistics flow.Add(new StatisticItemContainer(item) { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, }); } From 632077319f3e4763a6654a137c5723f5daba84c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 20:06:53 +0200 Subject: [PATCH 0686/2100] Fix taiko statistics table not using 2-column layout --- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index feb8b0c2c2..584a91bfdc 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -256,7 +256,7 @@ namespace osu.Game.Rulesets.Taiko RelativeSizeAxes = Axes.X, Height = 250 }, true), - new StatisticItem("Statistics", () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem("Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[] { new AverageHitError(timedHitEvents), new UnstableRate(timedHitEvents) From 4114fab8ea908ca5cd93d28f1671a7c35deb2f8e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 03:31:36 +0900 Subject: [PATCH 0687/2100] Update framework --- 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 759167829c..411a27b0b0 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -11,7 +11,7 @@ manifestmerger.jar - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7968364243..f147dfb6c1 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 2e691da079..71978e94fc 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -16,6 +16,6 @@ iossimulator-x64 - + From 4859b8c9947a3b6d9acb39767da6a84939ed0c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 20:49:34 +0200 Subject: [PATCH 0688/2100] Add some extra text coverage against potential emoji-to-link misparses --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 8 ++++++++ osu.Game.Tests/Visual/Online/TestSceneChatLink.cs | 1 + 2 files changed, 9 insertions(+) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 46dc47bf53..47f4410b07 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -510,6 +510,14 @@ namespace osu.Game.Tests.Chat Assert.AreEqual(result.Links.Count, 0); } + [Test] + public void TestEmojiWithSuccessiveParens() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "\uD83D\uDE10(let's hope this doesn't accidentally turn into a link)" }); + Assert.AreEqual("[emoji](let's hope this doesn't accidentally turn into a link)", result.DisplayContent); + Assert.AreEqual(result.Links.Count, 0); + } + [Test] public void TestAbsoluteExternalLinks() { diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index f5cf4c1ff2..ecf9b68297 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -89,6 +89,7 @@ namespace osu.Game.Tests.Visual.Online addMessageWithChecks($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel); addMessageWithChecks("Join my #english or #japanese channels.", 2, expectedActions: new[] { LinkAction.OpenChannel, LinkAction.OpenChannel }); addMessageWithChecks("Join my #english or #nonexistent #hashtag channels.", 1, expectedActions: LinkAction.OpenChannel); + addMessageWithChecks("Hello world\uD83D\uDE12(<--This is an emoji). There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20"); void addMessageWithChecks(string text, int linkAmount = 0, bool isAction = false, bool isImportant = false, params LinkAction[] expectedActions) { From ae7ed697ec6e49168b1f857cb16f885ce3699906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 21:22:48 +0200 Subject: [PATCH 0689/2100] Adjust failing test after permitting autoplay and no fail combo --- .../Visual/Navigation/TestSceneSkinEditorNavigation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index bedb2ceaa1..88904bf85b 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -148,7 +148,7 @@ namespace osu.Game.Tests.Visual.Navigation { advanceToSongSelect(); openSkinEditor(); - AddStep("select no fail and spun out", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModSpunOut() }); + AddStep("select relax and spun out", () => Game.SelectedMods.Value = new Mod[] { new OsuModRelax(), new OsuModSpunOut() }); switchToGameplayScene(); From a1e83b20e660f4c677859524a82ddff1d4977429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 21:23:57 +0200 Subject: [PATCH 0690/2100] Make autoplay compatible with `ModFailCondition` --- osu.Game/Rulesets/Mods/ModAutoplay.cs | 2 +- osu.Game/Rulesets/Mods/ModFailCondition.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index a4ffbeacef..88b41924ff 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mods public override bool ValidForMultiplayer => false; public override bool ValidForMultiplayerAsFreeMod => false; - public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModAdaptiveSpeed) }; + public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModAdaptiveSpeed) }; public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs index 97789b7f5a..e671c065cf 100644 --- a/osu.Game/Rulesets/Mods/ModFailCondition.cs +++ b/osu.Game/Rulesets/Mods/ModFailCondition.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mods { public abstract class ModFailCondition : Mod, IApplicableToHealthProcessor, IApplicableFailOverride { - public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) }; + public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax) }; [SettingSource("Restart on fail", "Automatically restarts when failed.")] public BindableBool Restart { get; } = new BindableBool(); From 674eb1a36b8bb211665616cba1ad233c84e24618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 21:25:43 +0200 Subject: [PATCH 0691/2100] Remove unused property --- osu.Game/Rulesets/Mods/ModAutoplay.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 88b41924ff..a3a4adc53d 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -20,8 +20,6 @@ namespace osu.Game.Rulesets.Mods public override LocalisableString Description => "Watch a perfect automated play through the song."; public override double ScoreMultiplier => 1; - public bool RestartOnFail => false; - public override bool UserPlayable => false; public override bool ValidForMultiplayer => false; public override bool ValidForMultiplayerAsFreeMod => false; From eeb50e27007dcf5076b4a0a99ad0e44d7226b8cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 21:45:06 +0200 Subject: [PATCH 0692/2100] Do not rely on `OverlayColourProvider` presence --- osu.Game/Overlays/RevertToDefaultButton.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/RevertToDefaultButton.cs b/osu.Game/Overlays/RevertToDefaultButton.cs index 48491c5d9c..28433f1669 100644 --- a/osu.Game/Overlays/RevertToDefaultButton.cs +++ b/osu.Game/Overlays/RevertToDefaultButton.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; @@ -33,7 +34,10 @@ namespace osu.Game.Overlays private Circle circle = null!; [Resolved] - private OverlayColourProvider colours { get; set; } = null!; + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider? colourProvider { get; set; } public Bindable Current { @@ -124,16 +128,16 @@ namespace osu.Game.Overlays { icon.RotateTo(-40, 500, Easing.OutQuint); - icon.FadeColour(colours.Light1, 300, Easing.OutQuint); - circle.FadeColour(colours.Background2, 300, Easing.OutQuint); + icon.FadeColour(colourProvider?.Light1 ?? colours.YellowLight, 300, Easing.OutQuint); + circle.FadeColour(colourProvider?.Background2 ?? colours.Gray6, 300, Easing.OutQuint); this.ScaleTo(1.2f, 300, Easing.OutQuint); } else { icon.RotateTo(0, 100, Easing.OutQuint); - icon.FadeColour(colours.Colour0, 100, Easing.OutQuint); - circle.FadeColour(colours.Background3, 100, Easing.OutQuint); + icon.FadeColour(colourProvider?.Colour0 ?? colours.Yellow, 100, Easing.OutQuint); + circle.FadeColour(colourProvider?.Background3 ?? colours.Gray3, 100, Easing.OutQuint); this.ScaleTo(1f, 100, Easing.OutQuint); } } From 6b9dabbd3c776800f10e0ccad7253fda22edec5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 21:54:28 +0200 Subject: [PATCH 0693/2100] Rewrite test scene to show non-themed and themed variants --- .../TestSceneRevertToDefaultButton.cs | 51 ++++++++----------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs b/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs index bfef120358..0e31757396 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs @@ -2,58 +2,49 @@ // See the LICENCE file in the repository root for full licence text. 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; +using osu.Framework.Testing; using osu.Game.Overlays; +using osu.Game.Tests.Visual.UserInterface; using osuTK; namespace osu.Game.Tests.Visual.Settings { - public partial class TestSceneRevertToDefaultButton : OsuTestScene + public partial class TestSceneRevertToDefaultButton : ThemeComparisonTestScene { private float scale = 1; - [Cached] - private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - private readonly Bindable current = new Bindable { Default = default, Value = 1, }; - [Test] - public void TestBasic() + protected override Drawable CreateContent() => new Container { - RevertToDefaultButton revertToDefaultButton = null!; - - AddStep("create button", () => Child = new Container + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = new RevertToDefaultButton { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background2, - }, - revertToDefaultButton = new RevertToDefaultButton - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(scale), - Current = current, - } - } - }); + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(scale), + Current = current, + } + }; + + [Test] + public void TestStates() + { + CreateThemedContent(OverlayColourScheme.Purple); AddSliderStep("set scale", 1, 4, 1, scale => { this.scale = scale; - if (revertToDefaultButton != null) - revertToDefaultButton.Scale = new Vector2(scale); + foreach (var revertToDefaultButton in this.ChildrenOfType>()) + revertToDefaultButton.Parent!.Scale = new Vector2(scale); }); AddToggleStep("toggle default state", state => current.Value = state ? default : 1); AddToggleStep("toggle disabled state", state => current.Disabled = state); From 3ca767b7a26564e8ed07903a689cb9660585261f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 21:58:55 +0200 Subject: [PATCH 0694/2100] Remove outdated comment --- osu.Game/Overlays/RevertToDefaultButton.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/RevertToDefaultButton.cs b/osu.Game/Overlays/RevertToDefaultButton.cs index 28433f1669..94d56971e0 100644 --- a/osu.Game/Overlays/RevertToDefaultButton.cs +++ b/osu.Game/Overlays/RevertToDefaultButton.cs @@ -64,7 +64,6 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load() { - // size intentionally much larger than actual drawn content, so that the button is easier to click. Size = new Vector2(14); AddRange(new Drawable[] From 9cc2150b5a37dfb0ecf940f31489dbf20c157384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 22:00:21 +0200 Subject: [PATCH 0695/2100] Fix mixed `[cC]urrent` usage --- osu.Game/Overlays/RevertToDefaultButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/RevertToDefaultButton.cs b/osu.Game/Overlays/RevertToDefaultButton.cs index 94d56971e0..c43d233fc1 100644 --- a/osu.Game/Overlays/RevertToDefaultButton.cs +++ b/osu.Game/Overlays/RevertToDefaultButton.cs @@ -121,7 +121,7 @@ namespace osu.Game.Overlays Enabled.Value = !current.Disabled; - this.FadeTo(current.Disabled ? 0.2f : (Current.IsDefault ? 0 : 1), fade_duration, Easing.OutQuint); + this.FadeTo(current.Disabled ? 0.2f : (current.IsDefault ? 0 : 1), fade_duration, Easing.OutQuint); if (IsHovered && Enabled.Value) { From e7dbe3c9380b664dc1a67ad8b36eeabd6cd4a131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 22:28:26 +0200 Subject: [PATCH 0696/2100] Fix broken test --- .../Visual/Settings/TestSceneRevertToDefaultButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs b/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs index 0e31757396..d081f663ba 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Settings [Test] public void TestStates() { - CreateThemedContent(OverlayColourScheme.Purple); + AddStep("create content", () => CreateThemedContent(OverlayColourScheme.Purple)); AddSliderStep("set scale", 1, 4, 1, scale => { this.scale = scale; From dd8774a64006792556b357d78756438324a34b37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 13:21:28 +0900 Subject: [PATCH 0697/2100] Vertically centre the editor osu! playfield --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index aac5f6ffb1..8d93613156 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -65,8 +65,8 @@ namespace osu.Game.Rulesets.Osu.Edit PlayfieldContentContainer.Padding = new MarginPadding { Vertical = 10, - Left = TOOLBOX_CONTRACTED_SIZE_LEFT + 10, - Right = TOOLBOX_CONTRACTED_SIZE_RIGHT + 10, + // Intentionally use the left toolbox size for both sides to vertically centre the playfield. + Horizontal = TOOLBOX_CONTRACTED_SIZE_LEFT + 10, }; LayerBelowRuleset.AddRange(new Drawable[] From 56acc9e3dde3f161c79f7fafc312d9800e0fc420 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 14:02:48 +0900 Subject: [PATCH 0698/2100] Change `BeatDivisorControl` to retrive bindable divisor via DI --- .../Visual/Editing/TestSceneBeatDivisorControl.cs | 9 +++++++-- .../Visual/Editing/TestSceneTimelineTickDisplay.cs | 2 +- .../Edit/Compose/Components/BeatDivisorControl.cs | 8 ++------ osu.Game/Screens/Edit/EditorScreenWithTimeline.cs | 10 ++-------- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs index 88b959a2a0..f2b3351533 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs @@ -22,7 +22,6 @@ namespace osu.Game.Tests.Visual.Editing public partial class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene { private BeatDivisorControl beatDivisorControl = null!; - private BindableBeatDivisor bindableBeatDivisor = null!; private SliderBar tickSliderBar => beatDivisorControl.ChildrenOfType>().Single(); private Triangle tickMarkerHead => tickSliderBar.ChildrenOfType().Single(); @@ -30,13 +29,19 @@ namespace osu.Game.Tests.Visual.Editing [Cached] private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + [Cached] + private readonly BindableBeatDivisor bindableBeatDivisor = new BindableBeatDivisor(16); + [SetUp] public void SetUp() => Schedule(() => { + bindableBeatDivisor.ValidDivisors.SetDefault(); + bindableBeatDivisor.SetDefault(); + Child = new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = beatDivisorControl = new BeatDivisorControl(bindableBeatDivisor = new BindableBeatDivisor(16)) + Child = beatDivisorControl = new BeatDivisorControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs index 1376ba23fb..18bd6d840a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Editing { BeatDivisor.Value = 4; - Add(new BeatDivisorControl(BeatDivisor) + Add(new BeatDivisorControl { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index f7159f8670..59b0bd1785 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -33,12 +33,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { private int? lastCustomDivisor; - private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); - - public BeatDivisorControl(BindableBeatDivisor beatDivisor) - { - this.beatDivisor.BindTo(beatDivisor); - } + [Resolved] + private BindableBeatDivisor beatDivisor { get; set; } = null!; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index b8cbff047e..0c5f28c2d6 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -3,7 +3,6 @@ #nullable disable -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -19,8 +18,6 @@ namespace osu.Game.Screens.Edit { private const float padding = 10; - private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); - private Container timelineContainer; protected EditorScreenWithTimeline(EditorScreenMode type) @@ -33,11 +30,8 @@ namespace osu.Game.Screens.Edit private LoadingSpinner spinner; [BackgroundDependencyLoader(true)] - private void load(OverlayColourProvider colourProvider, [CanBeNull] BindableBeatDivisor beatDivisor) + private void load(OverlayColourProvider colourProvider) { - if (beatDivisor != null) - this.beatDivisor.BindTo(beatDivisor); - Child = new GridContainer { RelativeSizeAxes = Axes.Both, @@ -82,7 +76,7 @@ namespace osu.Game.Screens.Edit AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Right = 5 }, }, - new BeatDivisorControl(this.beatDivisor) { RelativeSizeAxes = Axes.Both } + new BeatDivisorControl { RelativeSizeAxes = Axes.Both } }, }, RowDimensions = new[] From ebaf63b7644425dd06f9e1ba00e8159070813ccf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 14:04:12 +0900 Subject: [PATCH 0699/2100] Apply NRT to timeline related classes --- .../Edit/Compose/Components/Timeline/TimelineArea.cs | 8 +++----- osu.Game/Screens/Edit/EditorScreenWithTimeline.cs | 12 +++++------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index 0b83258f8b..02c773fd4b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,16 +16,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public partial class TimelineArea : CompositeDrawable { - public Timeline Timeline; + public Timeline Timeline = null!; private readonly Drawable userContent; - public TimelineArea(Drawable content = null) + public TimelineArea(Drawable? content = null) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - userContent = content ?? Drawable.Empty(); + userContent = content ?? Empty(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index 0c5f28c2d6..bd57ea42f0 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,17 +16,17 @@ namespace osu.Game.Screens.Edit { private const float padding = 10; - private Container timelineContainer; + private Container timelineContainer = null!; + + private Container mainContent = null!; + + private LoadingSpinner spinner = null!; protected EditorScreenWithTimeline(EditorScreenMode type) : base(type) { } - private Container mainContent; - - private LoadingSpinner spinner; - [BackgroundDependencyLoader(true)] private void load(OverlayColourProvider colourProvider) { From 5b2e70426488a295df83f83345326d7181b83568 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 14:19:04 +0900 Subject: [PATCH 0700/2100] Move beat divisor control inside of `TimelineArea` and adjust metrics to match design --- .../Components/Timeline/TimelineArea.cs | 44 +++++++++---------- .../Screens/Edit/EditorScreenWithTimeline.cs | 5 ++- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index 02c773fd4b..3969a002db 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -48,14 +48,24 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 140), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 30), + new Dimension(GridSizeMode.Absolute, 110), + }, Content = new[] { new Drawable[] { new Container { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, Name = @"Toggle controls", Children = new Drawable[] { @@ -92,31 +102,31 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } }, + Timeline = new Timeline(userContent), new Container { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, Name = @"Zoom controls", + Padding = new MarginPadding { Right = 5 }, Children = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background3, + Colour = colourProvider.Background2, }, new Container { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, Masking = true, Children = new[] { new TimelineButton { - RelativeSizeAxes = Axes.Y, - Height = 0.5f, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1, 0.5f), Icon = FontAwesome.Solid.SearchPlus, Action = () => Timeline.AdjustZoomRelatively(1) }, @@ -124,8 +134,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.Y, - Height = 0.5f, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1, 0.5f), Icon = FontAwesome.Solid.SearchMinus, Action = () => Timeline.AdjustZoomRelatively(-1) }, @@ -133,19 +143,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } }, - Timeline = new Timeline(userContent), + new BeatDivisorControl { RelativeSizeAxes = Axes.Both } }, }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - }, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } } }; diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index bd57ea42f0..a2367dd66c 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; -using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components.Timeline; namespace osu.Game.Screens.Edit @@ -30,6 +29,9 @@ namespace osu.Game.Screens.Edit [BackgroundDependencyLoader(true)] private void load(OverlayColourProvider colourProvider) { + // Grid with only two rows. + // First is the timeline area, which should be allowed to expand as required. + // Second is the main editor content, including the playfield and side toolbars (but not the bottom). Child = new GridContainer { RelativeSizeAxes = Axes.Both, @@ -74,7 +76,6 @@ namespace osu.Game.Screens.Edit AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Right = 5 }, }, - new BeatDivisorControl { RelativeSizeAxes = Axes.Both } }, }, RowDimensions = new[] From 1dc293ed619adc285ac5554b4ea6a65e04049f77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 14:24:53 +0900 Subject: [PATCH 0701/2100] Allow specifying a custom width for nubs in `OsuCheckbox`es --- osu.Game/Graphics/UserInterface/Nub.cs | 6 +++--- osu.Game/Graphics/UserInterface/OsuCheckbox.cs | 8 ++++---- osu.Game/Graphics/UserInterface/RoundedSliderBar.cs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/Nub.cs b/osu.Game/Graphics/UserInterface/Nub.cs index 28a2eb40c3..4b953718bc 100644 --- a/osu.Game/Graphics/UserInterface/Nub.cs +++ b/osu.Game/Graphics/UserInterface/Nub.cs @@ -20,16 +20,16 @@ namespace osu.Game.Graphics.UserInterface { public const float HEIGHT = 15; - public const float EXPANDED_SIZE = 50; + public const float DEFAULT_EXPANDED_SIZE = 50; private const float border_width = 3; private readonly Box fill; private readonly Container main; - public Nub() + public Nub(float expandedSize = DEFAULT_EXPANDED_SIZE) { - Size = new Vector2(EXPANDED_SIZE, HEIGHT); + Size = new Vector2(expandedSize, HEIGHT); InternalChildren = new[] { diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index 160105af1a..b7b405a7e8 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -47,7 +47,7 @@ namespace osu.Game.Graphics.UserInterface private Sample sampleChecked; private Sample sampleUnchecked; - public OsuCheckbox(bool nubOnRight = true) + public OsuCheckbox(bool nubOnRight = true, float nubSize = Nub.DEFAULT_EXPANDED_SIZE) { AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; @@ -61,7 +61,7 @@ namespace osu.Game.Graphics.UserInterface AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, }, - Nub = new Nub(), + Nub = new Nub(nubSize), new HoverSounds() }; @@ -70,14 +70,14 @@ namespace osu.Game.Graphics.UserInterface Nub.Anchor = Anchor.CentreRight; Nub.Origin = Anchor.CentreRight; Nub.Margin = new MarginPadding { Right = nub_padding }; - LabelTextFlowContainer.Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding * 2 }; + LabelTextFlowContainer.Padding = new MarginPadding { Right = Nub.DEFAULT_EXPANDED_SIZE + nub_padding * 2 }; } else { Nub.Anchor = Anchor.CentreLeft; Nub.Origin = Anchor.CentreLeft; Nub.Margin = new MarginPadding { Left = nub_padding }; - LabelTextFlowContainer.Padding = new MarginPadding { Left = Nub.EXPANDED_SIZE + nub_padding * 2 }; + LabelTextFlowContainer.Padding = new MarginPadding { Left = Nub.DEFAULT_EXPANDED_SIZE + nub_padding * 2 }; } Nub.Current.BindTo(Current); diff --git a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs index a666b83c05..56af23ff10 100644 --- a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs @@ -51,7 +51,7 @@ namespace osu.Game.Graphics.UserInterface public RoundedSliderBar() { Height = Nub.HEIGHT; - RangePadding = Nub.EXPANDED_SIZE / 2; + RangePadding = Nub.DEFAULT_EXPANDED_SIZE / 2; Children = new Drawable[] { new Container From 01750dd0914e13b6ce4ee1dda8c878639b7d9972 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 14:24:40 +0900 Subject: [PATCH 0702/2100] Update metrics of checkboxes and backgrounds to match design better --- .../Components/Timeline/TimelineArea.cs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index 3969a002db..70322bd3e0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -39,11 +39,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline InternalChildren = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background5 - }, new GridContainer { RelativeSizeAxes = Axes.X, @@ -76,24 +71,23 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, new FillFlowContainer { - AutoSizeAxes = Axes.Y, - Width = 160, + RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(10), Direction = FillDirection.Vertical, Spacing = new Vector2(0, 4), Children = new[] { - waveformCheckbox = new OsuCheckbox + waveformCheckbox = new OsuCheckbox(nubSize: 30f) { LabelText = EditorStrings.TimelineWaveform, Current = { Value = true }, }, - ticksCheckbox = new OsuCheckbox + ticksCheckbox = new OsuCheckbox(nubSize: 30f) { LabelText = EditorStrings.TimelineTicks, Current = { Value = true }, }, - controlPointsCheckbox = new OsuCheckbox + controlPointsCheckbox = new OsuCheckbox(nubSize: 30f) { LabelText = BeatmapsetsStrings.ShowStatsBpm, Current = { Value = true }, @@ -102,7 +96,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } }, - Timeline = new Timeline(userContent), + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue, + Colour = colourProvider.Background5 + }, + Timeline = new Timeline(userContent), + } + }, new Container { RelativeSizeAxes = Axes.Both, From 6b222cfafdedf6bb7299e189f30b533aa83b659f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 14:29:13 +0900 Subject: [PATCH 0703/2100] Fix slight misalignment so timeline is now completely centered --- osu.Game/Screens/Edit/EditorScreenWithTimeline.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index a2367dd66c..104d9dd58f 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.Edit { public abstract partial class EditorScreenWithTimeline : EditorScreen { - private const float padding = 10; + private const float padding = 15; private Container timelineContainer = null!; @@ -74,7 +74,6 @@ namespace osu.Game.Screens.Edit { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Right = 5 }, }, }, }, From 00e9746174b842bcec7cb6ea2c08bd2e55932580 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 15:17:24 +0900 Subject: [PATCH 0704/2100] Implement longer design for timing point piece --- .../Timeline/TimelineControlPointGroup.cs | 2 +- .../Components/Timeline/TopPointPiece.cs | 64 ++++++++++++++----- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs index fc3ef92bf5..c1b6069523 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline RelativeSizeAxes = Axes.Y; AutoSizeAxes = Axes.X; - Origin = Anchor.TopCentre; + Origin = Anchor.TopLeft; X = (float)group.Time; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs index 69fb001a66..243cdc6ddd 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -10,24 +8,25 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public partial class TopPointPiece : CompositeDrawable { - private readonly ControlPoint point; + protected readonly ControlPoint Point; - protected OsuSpriteText Label { get; private set; } + protected OsuSpriteText Label { get; private set; } = null!; + + private const float width = 80; public TopPointPiece(ControlPoint point) { - this.point = point; - AutoSizeAxes = Axes.X; + Point = point; + Width = width; Height = 16; - Margin = new MarginPadding(4); - - Masking = true; - CornerRadius = Height / 2; + Margin = new MarginPadding { Vertical = 4 }; Origin = Anchor.TopCentre; Anchor = Anchor.TopCentre; @@ -36,17 +35,52 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [BackgroundDependencyLoader] private void load(OsuColour colours) { + const float corner_radius = 4; + const float arrow_extension = 3; + const float triangle_portion = 15; + InternalChildren = new Drawable[] { - new Box + // This is a triangle, trust me. + // Doing it this way looks okay. Doing it using Triangle primitive is basically impossible. + new Container { - Colour = point.GetRepresentingColour(colours), - RelativeSizeAxes = Axes.Both, + Colour = Point.GetRepresentingColour(colours), + X = -corner_radius, + Size = new Vector2(triangle_portion * arrow_extension, Height), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Masking = true, + CornerRadius = Height, + CornerExponent = 1.4f, + Children = new Drawable[] + { + new Box + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.Y, + Width = width - triangle_portion, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = Point.GetRepresentingColour(colours), + Masking = true, + CornerRadius = corner_radius, + Child = new Box + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + }, }, Label = new OsuSpriteText { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Padding = new MarginPadding(3), Font = OsuFont.Default.With(size: 14, weight: FontWeight.SemiBold), Colour = colours.B5, From 57abb15724e537bee9a97ffdbd810b632b675671 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 15:58:53 +0900 Subject: [PATCH 0705/2100] Update design of timeline centre marker and adjust surrounding paddings --- .../Components/Timeline/CentreMarker.cs | 20 +++++++------------ .../Components/Timeline/TimelineArea.cs | 20 +++++++++++++------ .../Screens/Edit/EditorScreenWithTimeline.cs | 4 ++-- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs index aee3cffbfd..74786cc0c9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs @@ -12,17 +12,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public partial class CentreMarker : CompositeDrawable { - private const float triangle_width = 15; - private const float triangle_height = 10; - private const float bar_width = 2; + private const float triangle_width = 8; + + private const float bar_width = 1.6f; public CentreMarker() { RelativeSizeAxes = Axes.Y; Size = new Vector2(triangle_width, 1); - Anchor = Anchor.Centre; - Origin = Anchor.Centre; + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; InternalChildren = new Drawable[] { @@ -37,22 +37,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, - Size = new Vector2(triangle_width, triangle_height), + Size = new Vector2(triangle_width, triangle_width * 0.8f), Scale = new Vector2(1, -1) }, - new Triangle - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Size = new Vector2(triangle_width, triangle_height), - } }; } [BackgroundDependencyLoader] private void load(OsuColour colours) { - Colour = colours.RedDark; + Colour = colours.Red1; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index 70322bd3e0..fb7ce8e423 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Overlays; @@ -29,10 +30,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider, OsuColour colours) { - Masking = true; - OsuCheckbox waveformCheckbox; OsuCheckbox controlPointsCheckbox; OsuCheckbox ticksCheckbox; @@ -51,7 +50,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { new Dimension(GridSizeMode.Absolute, 140), new Dimension(), - new Dimension(GridSizeMode.Absolute, 30), + new Dimension(GridSizeMode.Absolute, 35), new Dimension(GridSizeMode.Absolute, 110), }, Content = new[] @@ -102,6 +101,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline AutoSizeAxes = Axes.Y, Children = new Drawable[] { + // the out-of-bounds portion of the centre marker. + new Box + { + Width = 24, + Height = EditorScreenWithTimeline.PADDING, + Depth = float.MaxValue, + Colour = colours.Red1, + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + }, new Box { RelativeSizeAxes = Axes.Both, @@ -115,7 +124,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { RelativeSizeAxes = Axes.Both, Name = @"Zoom controls", - Padding = new MarginPadding { Right = 5 }, + Padding = new MarginPadding { Right = 10 }, Children = new Drawable[] { new Box @@ -128,7 +137,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, - Masking = true, Children = new[] { new TimelineButton diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index 104d9dd58f..ea2790b50a 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.Edit { public abstract partial class EditorScreenWithTimeline : EditorScreen { - private const float padding = 15; + public const float PADDING = 10; private Container timelineContainer = null!; @@ -61,7 +61,7 @@ namespace osu.Game.Screens.Edit Name = "Timeline content", RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = padding, Top = padding }, + Padding = new MarginPadding { Horizontal = PADDING, Top = PADDING }, Child = new GridContainer { RelativeSizeAxes = Axes.X, From e6b8cd0c06abce3e02806c570b4316a484c2d817 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 16:55:12 +0900 Subject: [PATCH 0706/2100] Add editor header --- .../Edit/Components/Menus/EditorMenuBar.cs | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index a911b4e1d8..fa4e52d6a6 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -3,6 +3,9 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics; @@ -14,19 +17,56 @@ namespace osu.Game.Screens.Edit.Components.Menus { public partial class EditorMenuBar : OsuMenu { + private const float heading_area = 114; + public EditorMenuBar() : base(Direction.Horizontal, true) { RelativeSizeAxes = Axes.X; MaskingContainer.CornerRadius = 0; - ItemsContainer.Padding = new MarginPadding { Left = 100 }; + ItemsContainer.Padding = new MarginPadding { Left = heading_area }; } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider, TextureStore textures) { BackgroundColour = colourProvider.Background3; + + TextFlowContainer text; + + AddRangeInternal(new[] + { + new Container + { + RelativeSizeAxes = Axes.Y, + Width = heading_area, + Padding = new MarginPadding(8), + Children = new Drawable[] + { + new Sprite + { + Size = new Vector2(26), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Texture = textures.Get("Icons/Hexacons/editor"), + }, + text = new TextFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + } + } + }, + }); + + text.AddText("osu!", t => t.Font = OsuFont.TorusAlternate); + text.AddText("editor", t => + { + t.Font = OsuFont.TorusAlternate; + t.Colour = colourProvider.Highlight1; + }); } protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu(); From fe70f2492538533148fd3ee28a2749901736bf43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 17:00:42 +0900 Subject: [PATCH 0707/2100] Update design of summary timeline current time marker --- .../Timelines/Summary/Parts/MarkerPart.cs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs index d42c02e03d..ff707407dd 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs @@ -73,6 +73,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { public MarkerVisualisation() { + const float box_height = 4; + Anchor = Anchor.CentreLeft; Origin = Anchor.Centre; RelativePositionAxes = Axes.X; @@ -80,32 +82,46 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts AutoSizeAxes = Axes.X; InternalChildren = new Drawable[] { + new Box + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(14, box_height), + }, new Triangle { Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, Scale = new Vector2(1, -1), Size = new Vector2(10, 5), + Y = box_height, }, new Triangle { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Size = new Vector2(10, 5) + Size = new Vector2(10, 5), + Y = -box_height, + }, + new Box + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Size = new Vector2(14, box_height), }, new Box { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, - Width = 2, + Width = 1.4f, EdgeSmoothness = new Vector2(1, 0) } }; } [BackgroundDependencyLoader] - private void load(OsuColour colours) => Colour = colours.Red; + private void load(OsuColour colours) => Colour = colours.Red1; } } } From 85c780ae5be25c3b89ea414a2b03d688cefdd452 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 19:19:14 +0900 Subject: [PATCH 0708/2100] Allow the osu! logo to be proxied locally into scenes --- osu.Game/OsuGame.cs | 4 +-- osu.Game/Screens/Menu/MainMenu.cs | 7 ++++++ osu.Game/Screens/Menu/OsuLogo.cs | 41 +++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5b654e0c16..1a40bb8e3d 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -950,9 +950,9 @@ namespace osu.Game if (!args?.Any(a => a == @"--no-version-overlay") ?? true) loadComponentSingleFile(versionManager = new VersionManager { Depth = int.MinValue }, ScreenContainer.Add); - loadComponentSingleFile(osuLogo, logo => + loadComponentSingleFile(osuLogo, _ => { - logoContainer.Add(logo); + osuLogo.SetupDefaultContainer(logoContainer); // Loader has to be created after the logo has finished loading as Loader performs logo transformations on entering. ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both)); diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index baa34d4914..510c9a5373 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Logging; @@ -85,6 +86,7 @@ namespace osu.Game.Screens.Menu private ParallaxContainer buttonsContainer; private SongTicker songTicker; + private Container logoTarget; [BackgroundDependencyLoader(true)] private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics) @@ -129,6 +131,7 @@ namespace osu.Game.Screens.Menu } } }, + logoTarget = new Container { RelativeSizeAxes = Axes.Both, }, sideFlashes = new MenuSideFlashes(), songTicker = new SongTicker { @@ -208,6 +211,8 @@ namespace osu.Game.Screens.Menu logo.FadeColour(Color4.White, 100, Easing.OutQuint); logo.FadeIn(100, Easing.OutQuint); + logo.ProxyToContainer(logoTarget); + if (resuming) { Buttons.State = ButtonSystemState.TopLevel; @@ -245,6 +250,8 @@ namespace osu.Game.Screens.Menu var seq = logo.FadeOut(300, Easing.InSine) .ScaleTo(0.2f, 300, Easing.InSine); + logo.ReturnProxy(); + seq.OnComplete(_ => Buttons.SetOsuLogo(null)); seq.OnAbort(_ => Buttons.SetOsuLogo(null)); } diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 277b8bf888..8867ecfb2a 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -435,5 +435,46 @@ namespace osu.Game.Screens.Menu logoBounceContainer.MoveTo(Vector2.Zero, 800, Easing.OutElastic); base.OnDragEnd(e); } + + private Container defaultProxyTarget; + private Container currentProxyTarget; + private Drawable proxy; + + public Drawable ProxyToContainer(Container c) + { + if (currentProxyTarget != null) + throw new InvalidOperationException("Previous proxy usage was not returned"); + + if (defaultProxyTarget == null) + throw new InvalidOperationException($"{nameof(SetupDefaultContainer)} must be called first"); + + currentProxyTarget = c; + + defaultProxyTarget.Remove(proxy, false); + currentProxyTarget.Add(proxy); + return proxy; + } + + public void ReturnProxy() + { + if (currentProxyTarget == null) + throw new InvalidOperationException("No usage to return"); + + if (defaultProxyTarget == null) + throw new InvalidOperationException($"{nameof(SetupDefaultContainer)} must be called first"); + + currentProxyTarget.Remove(proxy, false); + currentProxyTarget = null; + + defaultProxyTarget.Add(proxy); + } + + public void SetupDefaultContainer(Container container) + { + defaultProxyTarget = container; + + defaultProxyTarget.Add(this); + defaultProxyTarget.Add(proxy = CreateProxy()); + } } } From d4fb0bef95cd9ede8cb8da9df98dfc07c7791833 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 19:29:36 +0900 Subject: [PATCH 0709/2100] Fix osu!mania scores failing to convert to new standardised score due to cast failure Regressed in https://github.com/ppy/osu/pull/23917. Closes #24217. --- .../Scoring/ManiaScoreProcessor.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index a0f6ac572d..16e10006e3 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Scoring } protected override IEnumerable EnumerateHitObjects(IBeatmap beatmap) - => base.EnumerateHitObjects(beatmap).OrderBy(ho => (ManiaHitObject)ho, JudgementOrderComparer.DEFAULT); + => base.EnumerateHitObjects(beatmap).OrderBy(ho => ho, JudgementOrderComparer.DEFAULT); protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) { @@ -34,11 +34,11 @@ namespace osu.Game.Rulesets.Mania.Scoring protected override double GetComboScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base)); - private class JudgementOrderComparer : IComparer + private class JudgementOrderComparer : IComparer { public static readonly JudgementOrderComparer DEFAULT = new JudgementOrderComparer(); - public int Compare(ManiaHitObject? x, ManiaHitObject? y) + public int Compare(HitObject? x, HitObject? y) { if (ReferenceEquals(x, y)) return 0; if (ReferenceEquals(x, null)) return -1; @@ -48,11 +48,14 @@ namespace osu.Game.Rulesets.Mania.Scoring if (result != 0) return result; - // due to the way input is handled in mania, notes take precedence over ticks in judging order. - if (x is Note && y is not Note) return -1; - if (x is not Note && y is Note) return 1; + var xNote = x as Note; + var yNote = y as Note; - return x.Column.CompareTo(y.Column); + // due to the way input is handled in mania, notes take precedence over ticks in judging order. + if (xNote != null && yNote == null) return -1; + if (xNote == null && yNote != null) return 1; + + return xNote!.Column.CompareTo(yNote!.Column); } } } From 480259b8d2b4cfc2f2e06e13eaab6ea1f2162dd1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 20:02:25 +0900 Subject: [PATCH 0710/2100] Ensure migration is never run on scores with new-enough `TotalScoreVersion`s --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index b8afdad294..11a5e252a3 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -26,6 +26,9 @@ namespace osu.Game.Database if (score.IsLegacyScore) return false; + if (score.TotalScoreVersion > 30000002) + return false; + // Recalculate the old-style standardised score to see if this was an old lazer score. bool oldScoreMatchesExpectations = GetOldStandardised(score) == score.TotalScore; // Some older scores don't have correct statistics populated, so let's give them benefit of doubt. From 1868826d692f9d0c0fc13a9184758a1c3518a85f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 15 Jul 2023 01:12:10 +0900 Subject: [PATCH 0711/2100] Update framework --- 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 411a27b0b0..49b9de678d 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -11,7 +11,7 @@ manifestmerger.jar - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 0ccf851138..f56133e64c 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 71978e94fc..12f84c66c2 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -16,6 +16,6 @@ iossimulator-x64 - + From e58488d04a64acb9a08cee6253da5973d2c2c9bd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 15 Jul 2023 11:19:34 +0900 Subject: [PATCH 0712/2100] Fix incorrect comparer implementation --- .../Scoring/ManiaScoreProcessor.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 16e10006e3..c53f3c3e07 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -48,14 +48,13 @@ namespace osu.Game.Rulesets.Mania.Scoring if (result != 0) return result; - var xNote = x as Note; - var yNote = y as Note; - // due to the way input is handled in mania, notes take precedence over ticks in judging order. - if (xNote != null && yNote == null) return -1; - if (xNote == null && yNote != null) return 1; + if (x is Note && y is not Note) return -1; + if (x is not Note && y is Note) return 1; - return xNote!.Column.CompareTo(yNote!.Column); + return x is ManiaHitObject maniaX && y is ManiaHitObject maniaY + ? maniaX.Column.CompareTo(maniaY.Column) + : 0; } } } From eb81eac635401bde3c923f03a1a7238c9b03caf3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 15 Jul 2023 12:19:18 +0900 Subject: [PATCH 0713/2100] Flag decoded scores more correctly --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index c6461840aa..8c00110909 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -49,6 +49,13 @@ namespace osu.Game.Scoring.Legacy scoreInfo.IsLegacyScore = version < LegacyScoreEncoder.FIRST_LAZER_VERSION; + // TotalScoreVersion gets initialised to LATEST_VERSION. + // In the case where the incoming score has either an osu!stable or old lazer version, we need + // to mark it with the correct version increment to trigger reprocessing to new standardised scoring. + // + // See StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(). + scoreInfo.TotalScoreVersion = version < 30000002 ? 30000001 : LegacyScoreEncoder.LATEST_VERSION; + string beatmapHash = sr.ReadString(); workingBeatmap = GetBeatmap(beatmapHash); From 7f957d3fbef45d745d9ebc545eefa6050fa3b84f Mon Sep 17 00:00:00 2001 From: chayleaf Date: Sat, 15 Jul 2023 20:24:40 +0700 Subject: [PATCH 0714/2100] Fix some taiko maps not finishing in some conditions I don't know how to reproduce this issue in a test, so no tests for now. Nonetheless, this fixes the issue for me at least on one map: https://osu.ppy.sh/beatmapsets/1899665#taiko/3915653 This workaround is similar to #16475 (the test from that commit got eventually removed for some reason). --- .../Objects/Drawables/DrawableStrongNestedHit.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index 9b410d1871..eb31708e20 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs @@ -16,5 +16,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables : base(nestedHit) { } + + public override void OnKilled() + { + base.OnKilled(); + + if (!Judged) + ApplyResult(r => r.Type = r.Judgement.MinResult); + } } } From 309c8522227d63d647847ad0cf90ccf1f5ffd6eb Mon Sep 17 00:00:00 2001 From: Aki <75532970+AkiSakurai@users.noreply.github.com> Date: Sat, 15 Jul 2023 22:12:48 +0800 Subject: [PATCH 0715/2100] Compute the top local rank directly without an expensive detach call --- osu.Game/Scoring/ScoreInfoExtensions.cs | 8 ++++++++ osu.Game/Screens/Select/Carousel/TopLocalRank.cs | 3 +-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs index 6e57a9fd0b..63cc077cde 100644 --- a/osu.Game/Scoring/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/ScoreInfoExtensions.cs @@ -32,5 +32,13 @@ namespace osu.Game.Scoring /// The to compute the maximum achievable combo for. /// The maximum achievable combo. public static int GetMaximumAchievableCombo(this ScoreInfo score) => score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value); + + /// + /// Retrieves the with the maximum total score. + /// + /// An array of s to retrieve the scoreInfo with maximum total score. + /// The instance with the maximum total score. + public static ScoreInfo? MaxByTopScore(this IEnumerable scores) + => scores.MaxBy(info => (info.TotalScore, -info.OnlineID, -info.Date.UtcDateTime.Ticks)); } } diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index c17de77619..fe2d79b080 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -75,7 +74,7 @@ namespace osu.Game.Screens.Select.Carousel if (changes?.HasCollectionChanges() == false) return; - ScoreInfo? topScore = sender.Detach().OrderByTotalScore().FirstOrDefault(); + ScoreInfo? topScore = sender.MaxByTopScore(); updateable.Rank = topScore?.Rank; updateable.Alpha = topScore != null ? 1 : 0; From 3e91d308254d63125e47a3a92f6d52bc9c2d551e Mon Sep 17 00:00:00 2001 From: chayleaf Date: Sat, 15 Jul 2023 22:33:16 +0700 Subject: [PATCH 0716/2100] move StrongNestedHit OnKilled to DrawableStrongNestedHit --- .../Objects/Drawables/DrawableDrumRoll.cs | 8 -------- .../Objects/Drawables/DrawableDrumRollTick.cs | 8 -------- .../Objects/Drawables/DrawableStrongNestedHit.cs | 3 ++- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 005d2ab1ac..2bf0c04adf 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -195,14 +195,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); } - public override void OnKilled() - { - base.OnKilled(); - - if (Time.Current > ParentHitObject.HitObject.GetEndTime() && !Judged) - ApplyResult(r => r.Type = r.Judgement.MinResult); - } - public override bool OnPressed(KeyBindingPressEvent e) => false; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index abecd19545..c900165d34 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -108,14 +108,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); } - public override void OnKilled() - { - base.OnKilled(); - - if (Time.Current > ParentHitObject.HitObject.GetEndTime() && !Judged) - ApplyResult(r => r.Type = r.Judgement.MinResult); - } - public override bool OnPressed(KeyBindingPressEvent e) => false; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index eb31708e20..4d430987b9 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.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 osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.OnKilled(); - if (!Judged) + if (!Judged && Time.Current > ParentHitObject?.HitObject.GetEndTime()) ApplyResult(r => r.Type = r.Judgement.MinResult); } } From 542916f8570eb757870ab3d612edff9612c1bfee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 15 Jul 2023 18:15:42 +0200 Subject: [PATCH 0717/2100] Bring back test coverage for fail case from #16475 It was inadvertently dropped during refactoring in b185194d07f0ff1e6ebe7eafb796a85491fab118. --- .../Judgements/TestSceneDrumRollJudgements.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs index 21f2b8f1be..2f9f5e0a37 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs @@ -75,6 +75,25 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements AssertResult(0, HitResult.IgnoreHit); } + [Test] + public void TestHitNoneStrongDrumRoll() + { + PerformTest(new List + { + new TaikoReplayFrame(0), + }, CreateBeatmap(createDrumRoll(true))); + + AssertJudgementCount(12); + + for (int i = 0; i < 5; ++i) + { + AssertResult(i, HitResult.IgnoreMiss); + AssertResult(i, HitResult.IgnoreMiss); + } + + AssertResult(0, HitResult.IgnoreHit); + } + [Test] public void TestHitAllStrongDrumRollWithOneKey() { From 24d63a4d96c99dc0ff55e50929a3f7b96166e5eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 15 Jul 2023 18:17:45 +0200 Subject: [PATCH 0718/2100] Add test coverage for failing hit judgement with HD active --- .../Judgements/JudgementTest.cs | 5 +++- .../Judgements/TestSceneHitJudgements.cs | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs index eb2d96ec51..f3e37736b2 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.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.Linq; using NUnit.Framework; @@ -10,6 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; @@ -36,11 +38,12 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements () => Is.EqualTo(expectedResult)); } - protected void PerformTest(List frames, Beatmap? beatmap = null) + protected void PerformTest(List frames, Beatmap? beatmap = null, Mod[]? mods = null) { AddStep("load player", () => { Beatmap.Value = CreateWorkingBeatmap(beatmap); + SelectedMods.Value = mods ?? Array.Empty(); var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs index 3bf94eb62e..b9e767e625 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs @@ -6,8 +6,10 @@ using NUnit.Framework; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Replays; +using osu.Game.Rulesets.Taiko.Scoring; namespace osu.Game.Rulesets.Taiko.Tests.Judgements { @@ -157,5 +159,31 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements AssertJudgementCount(1); AssertResult(0, HitResult.Ok); } + + [Test] + public void TestStrongHitWithHidden() + { + const double hit_time = 1000; + + var beatmap = CreateBeatmap(new Hit + { + Type = HitType.Centre, + StartTime = hit_time, + IsStrong = true + }); + + var hitWindows = new TaikoHitWindows(); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + + PerformTest(new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) - 1, TaikoAction.LeftCentre), + }, beatmap, new[] { new TaikoModHidden() }); + + AssertJudgementCount(2); + AssertResult(0, HitResult.Ok); + AssertResult(0, HitResult.IgnoreMiss); + } } } From 9e960894c2b03029a0d40d20a7c0cf9ef451ce5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 15 Jul 2023 18:22:04 +0200 Subject: [PATCH 0719/2100] Add inline commentary about `OnKilled()` override --- .../Objects/Drawables/DrawableStrongNestedHit.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index 4d430987b9..724d59edcd 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs @@ -22,6 +22,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.OnKilled(); + // usually, the strong nested hit isn't judged itself, it is judged by its parent object. + // however, in rare cases (see: drum rolls, hits with hidden active), + // it can happen that the hit window of the nested strong hit extends past the lifetime of the parent object. + // this is a safety to prevent such cases from causing the nested hit to never be judged and as such prevent gameplay from completing. if (!Judged && Time.Current > ParentHitObject?.HitObject.GetEndTime()) ApplyResult(r => r.Type = r.Judgement.MinResult); } From 955aa70e46835d6200a4fabb5ba18d5bda1b5787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 15 Jul 2023 18:43:31 +0200 Subject: [PATCH 0720/2100] Add failing test case for hitting nested hit past parent end time --- .../Judgements/TestSceneHitJudgements.cs | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs index b9e767e625..5e9d4bcf14 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs @@ -8,6 +8,7 @@ using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Replays; using osu.Game.Rulesets.Taiko.Scoring; @@ -161,7 +162,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements } [Test] - public void TestStrongHitWithHidden() + public void TestStrongHitOneKeyWithHidden() { const double hit_time = 1000; @@ -185,5 +186,32 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements AssertResult(0, HitResult.Ok); AssertResult(0, HitResult.IgnoreMiss); } + + [Test] + public void TestStrongHitTwoKeysWithHidden() + { + const double hit_time = 1000; + + var beatmap = CreateBeatmap(new Hit + { + Type = HitType.Centre, + StartTime = hit_time, + IsStrong = true + }); + + var hitWindows = new TaikoHitWindows(); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + + PerformTest(new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) - 1, TaikoAction.LeftCentre), + new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) + DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW - 2, TaikoAction.LeftCentre, TaikoAction.RightCentre), + }, beatmap, new[] { new TaikoModHidden() }); + + AssertJudgementCount(2); + AssertResult(0, HitResult.Ok); + AssertResult(0, HitResult.LargeBonus); + } } } From 041e81889209c0a568a42b38227b2cedf9c2d3e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 15 Jul 2023 18:44:47 +0200 Subject: [PATCH 0721/2100] Fix nested hits not being hittable if cut off by parent object ending --- osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index 4708ef9bf0..2c3b4a8d18 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -62,6 +62,8 @@ namespace osu.Game.Rulesets.Taiko.Mods hitObject.LifetimeEnd = state == ArmedState.Idle || !hitObject.AllJudged ? hitObject.HitObject.GetEndTime() + hitObject.HitObject.HitWindows.WindowFor(HitResult.Miss) : hitObject.HitStateUpdateTime; + // extend the lifetime end of the object in order to allow its nested strong hit (if any) to be judged. + hitObject.LifetimeEnd += DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW; } break; From cb354685ca323f4e429e60617077ed5a5175ee3d Mon Sep 17 00:00:00 2001 From: Aki <75532970+AkiSakurai@users.noreply.github.com> Date: Sun, 16 Jul 2023 10:21:32 +0800 Subject: [PATCH 0722/2100] simplify code --- osu.Game/Scoring/ScoreInfoExtensions.cs | 8 -------- osu.Game/Screens/Select/Carousel/TopLocalRank.cs | 4 ++-- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs index 63cc077cde..6e57a9fd0b 100644 --- a/osu.Game/Scoring/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/ScoreInfoExtensions.cs @@ -32,13 +32,5 @@ namespace osu.Game.Scoring /// The to compute the maximum achievable combo for. /// The maximum achievable combo. public static int GetMaximumAchievableCombo(this ScoreInfo score) => score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value); - - /// - /// Retrieves the with the maximum total score. - /// - /// An array of s to retrieve the scoreInfo with maximum total score. - /// The instance with the maximum total score. - public static ScoreInfo? MaxByTopScore(this IEnumerable scores) - => scores.MaxBy(info => (info.TotalScore, -info.OnlineID, -info.Date.UtcDateTime.Ticks)); } } diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index fe2d79b080..da9661f702 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -74,8 +75,7 @@ namespace osu.Game.Screens.Select.Carousel if (changes?.HasCollectionChanges() == false) return; - ScoreInfo? topScore = sender.MaxByTopScore(); - + ScoreInfo? topScore = sender.MaxBy(info => (info.TotalScore, -info.Date.UtcDateTime.Ticks)); updateable.Rank = topScore?.Rank; updateable.Alpha = topScore != null ? 1 : 0; } From 416ee0d99cb0ddb344ec6183fb15278f45295b2b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 16 Jul 2023 12:48:58 +0900 Subject: [PATCH 0723/2100] Fix covariant array spec in new test --- .../Judgements/TestSceneHitJudgements.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs index 5e9d4bcf14..32966905c9 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Mods; @@ -180,7 +181,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements { new TaikoReplayFrame(0), new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) - 1, TaikoAction.LeftCentre), - }, beatmap, new[] { new TaikoModHidden() }); + }, beatmap, new Mod[] { new TaikoModHidden() }); AssertJudgementCount(2); AssertResult(0, HitResult.Ok); @@ -207,7 +208,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements new TaikoReplayFrame(0), new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) - 1, TaikoAction.LeftCentre), new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) + DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW - 2, TaikoAction.LeftCentre, TaikoAction.RightCentre), - }, beatmap, new[] { new TaikoModHidden() }); + }, beatmap, new Mod[] { new TaikoModHidden() }); AssertJudgementCount(2); AssertResult(0, HitResult.Ok); From ce12afde70e437fe1e03ebee24d70258a1dd3063 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 15 Jul 2023 23:38:06 -0700 Subject: [PATCH 0724/2100] Remove android manifest overlay --- osu.Android.props | 4 ---- osu.Android/Properties/AndroidManifestOverlay.xml | 15 --------------- 2 files changed, 19 deletions(-) delete mode 100644 osu.Android/Properties/AndroidManifestOverlay.xml diff --git a/osu.Android.props b/osu.Android.props index 49b9de678d..f5d986a458 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -8,14 +8,10 @@ true true - manifestmerger.jar - - - diff --git a/osu.Android/Properties/AndroidManifestOverlay.xml b/osu.Android/Properties/AndroidManifestOverlay.xml deleted file mode 100644 index 815f935383..0000000000 --- a/osu.Android/Properties/AndroidManifestOverlay.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file From 72bf43e297896f6ad4cd76644dc232269a435e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 16 Jul 2023 16:28:44 +0200 Subject: [PATCH 0725/2100] Add failing test covering exit dialog crash --- .../Navigation/TestSceneScreenNavigation.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index c3b668f591..8e335b2345 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -722,6 +722,33 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("Wait for game exit", () => Game.ScreenStack.CurrentScreen == null); } + [Test] + public void TestForceExitWithOperationInProgress() + { + AddStep("set hold delay to 0", () => Game.LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0)); + AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); + + ProgressNotification progressNotification = null!; + + AddStep("start ongoing operation", () => + { + progressNotification = new ProgressNotification + { + Text = "Something is still running", + Progress = 0.5f, + State = ProgressNotificationState.Active, + }; + Game.Notifications.Post(progressNotification); + }); + + AddStep("attempt exit", () => + { + for (int i = 0; i < 2; ++i) + Game.ScreenStack.CurrentScreen.Exit(); + }); + AddUntilStep("stopped at exit confirm", () => Game.ChildrenOfType().Single().CurrentDialog is ConfirmExitDialog); + } + [Test] public void TestExitGameFromSongSelect() { From d25a03984beba1ba009f4120920ced2820e3e122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 16 Jul 2023 16:29:27 +0200 Subject: [PATCH 0726/2100] Fix `PerformAction()` potentially crashing when no matching button is found --- osu.Game/Overlays/Dialog/PopupDialog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index e1e5604e4c..d7316305cc 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -229,7 +229,7 @@ namespace osu.Game.Overlays.Dialog { // Buttons are regularly added in BDL or LoadComplete, so let's schedule to ensure // they are ready to be pressed. - Schedule(() => Buttons.OfType().First().TriggerClick()); + Schedule(() => Buttons.OfType().FirstOrDefault()?.TriggerClick()); } protected override bool OnKeyDown(KeyDownEvent e) From a42992d3fdc4a8ea4762a9f546282aea66e5eb42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 16 Jul 2023 17:10:26 +0200 Subject: [PATCH 0727/2100] Remove unused local variable --- .../Visual/Navigation/TestSceneScreenNavigation.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 8e335b2345..3acc2f0384 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -728,17 +728,14 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("set hold delay to 0", () => Game.LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0)); AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); - ProgressNotification progressNotification = null!; - AddStep("start ongoing operation", () => { - progressNotification = new ProgressNotification + Game.Notifications.Post(new ProgressNotification { Text = "Something is still running", Progress = 0.5f, State = ProgressNotificationState.Active, - }; - Game.Notifications.Post(progressNotification); + }); }); AddStep("attempt exit", () => From e25cd03e4bc2d831086edfcf863fef34a9a101c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Jul 2023 00:55:25 +0900 Subject: [PATCH 0728/2100] Update framework --- 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 49b9de678d..7200f45277 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -11,7 +11,7 @@ manifestmerger.jar - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f56133e64c..16c8160fb6 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 12f84c66c2..01ab409fe4 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -16,6 +16,6 @@ iossimulator-x64 - + From a6b1559e1f414a608be1f315bf8527877c219e3c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Jul 2023 02:24:51 +0900 Subject: [PATCH 0729/2100] Update `DiscordRichPresence` to pull in performance fix See https://github.com/Lachee/discord-rpc-csharp/pull/237 --- osu.Desktop/osu.Desktop.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 16d6a81d40..25f4cff00e 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -26,7 +26,7 @@ - + From cd02a8a9ca599b26304f3ef7b39796bbf2c8a68b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 16 Jul 2023 19:43:37 +0200 Subject: [PATCH 0730/2100] Fix `PopupDialog` potentially accumulating schedules during load --- osu.Game/Overlays/Dialog/PopupDialog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index d7316305cc..2e9087fdbd 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -229,7 +229,7 @@ namespace osu.Game.Overlays.Dialog { // Buttons are regularly added in BDL or LoadComplete, so let's schedule to ensure // they are ready to be pressed. - Schedule(() => Buttons.OfType().FirstOrDefault()?.TriggerClick()); + Scheduler.AddOnce(() => Buttons.OfType().FirstOrDefault()?.TriggerClick()); } protected override bool OnKeyDown(KeyDownEvent e) From 7fbd47e9eec893409211a4a1f49e9819f0ded742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 16 Jul 2023 19:56:22 +0200 Subject: [PATCH 0731/2100] Fix `MultiplayerMatchSubScreen` erroneously pushing exit dialog on API failure If `IAPIProvider.State` changes from `Online` at any point when being on an `OnlinePlayScreen`, it will be forcefully exited from. However, `MultiplayerMatchSubScreen` had local logic that suppressed the exit in order to show a confirmation dialog. The problem is, that in the suppression logic, `MultiplayerMatchSubScreen` was checking `MultiplayerClient.IsConnected`, which is a SignalR flag, and was not checking `IAPIAccess.State`, which is maintained separately. Due to differing timeouts and failure thresholds, it is not impossible to have `MultiplayerClient.IsConnected == true` but `IAPIAccess.State != APIState.Online`. In such a case, the match subscreen would wrongly consider itself to be still online and due to that, push useless confirmation dialogs, while being in the process of being forcefully exited. This then caused the dialog to cause a crash, as it was calling `.Exit()` on the screen which would already have been exited by that point, by the force-exit flow. --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index ecf38a956d..01f04b44c9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -17,6 +17,7 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Cursor; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -49,6 +50,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } + [Resolved] + private IAPIProvider api { get; set; } + [Resolved(canBeNull: true)] private OsuGame game { get; set; } @@ -251,10 +255,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public override bool OnExiting(ScreenExitEvent e) { // the room may not be left immediately after a disconnection due to async flow, - // so checking the IsConnected status is also required. - if (client.Room == null || !client.IsConnected.Value) + // so checking the MultiplayerClient / IAPIAccess statuses is also required. + if (client.Room == null || !client.IsConnected.Value || api.State.Value != APIState.Online) { - // room has not been created yet; exit immediately. + // room has not been created yet or we're offline; exit immediately. return base.OnExiting(e); } From 6200e207d292357656ceb4ad29bca093bcad9da1 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 16 Jul 2023 15:21:15 -0400 Subject: [PATCH 0732/2100] use fa_download for updates instead of fa_upload --- osu.Game/Updater/NoActionUpdateManager.cs | 4 ++-- osu.Game/Updater/SimpleUpdateManager.cs | 4 ++-- osu.Game/Updater/UpdateManager.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Updater/NoActionUpdateManager.cs b/osu.Game/Updater/NoActionUpdateManager.cs index 97d3275757..f776cd67be 100644 --- a/osu.Game/Updater/NoActionUpdateManager.cs +++ b/osu.Game/Updater/NoActionUpdateManager.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable @@ -47,7 +47,7 @@ namespace osu.Game.Updater { Text = $"A newer release of osu! has been found ({version} → {latestTagName}).\n\n" + "Check with your package manager / provider to bring osu! up-to-date!", - Icon = FontAwesome.Solid.Upload, + Icon = FontAwesome.Solid.Download, }); return true; diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index 1ecb73a154..bc1b0919b8 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable @@ -54,7 +54,7 @@ namespace osu.Game.Updater { Text = $"A newer release of osu! has been found ({version} → {latestTagName}).\n\n" + "Click here to download the new version, which can be installed over the top of your existing installation", - Icon = FontAwesome.Solid.Upload, + Icon = FontAwesome.Solid.Download, Activated = () => { host.OpenUrlExternally(getBestUrl(latest)); diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 47c2a169ed..190748137a 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -134,7 +134,7 @@ namespace osu.Game.Updater { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Icon = FontAwesome.Solid.Upload, + Icon = FontAwesome.Solid.Download, Size = new Vector2(34), Colour = OsuColour.Gray(0.2f), Depth = float.MaxValue, From 3888471148e0db66654e7ce4b34886671abe2828 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Sun, 16 Jul 2023 23:03:21 +0300 Subject: [PATCH 0733/2100] Add break length and bounds checks --- .../Editing/Checks/CheckBreaksTest.cs | 10 +++ osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 3 + osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs | 84 +++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs b/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs new file mode 100644 index 0000000000..664f72c5f8 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs @@ -0,0 +1,10 @@ +// 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.Tests.Editing.Checks +{ + public class CheckBreaksTest + { + + } +} diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index 3988f29e13..4bcf74db45 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -38,6 +38,9 @@ namespace osu.Game.Rulesets.Edit // Timing new CheckPreviewTime(), + + // Events + new CheckBreaks() }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs b/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs new file mode 100644 index 0000000000..12dc5554f4 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs @@ -0,0 +1,84 @@ +// 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 osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckBreaks : ICheck + { + // Breaks may be off by 1 ms. + private const int leniency_threshold = 1; + private const double min_start_threshold = 200; + + // Break end time depends on the upcoming object's pre-empt time. + // As things stand, "pre-empt time" is only defined for osu! standard + // This is a generic value representing AR=10 + // Relevant: https://github.com/ppy/osu/issues/14330#issuecomment-1002158551 + private const double min_end_threshold = 450; + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Events, "Breaks not achievable using the editor"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateEarlyStart(this), + new IssueTemplateLateEnd(this), + new IssueTemplateTooShort(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + foreach (var breakPeriod in context.Beatmap.Breaks) + { + if (breakPeriod.Duration < BreakPeriod.MIN_BREAK_DURATION) + yield return new IssueTemplateTooShort(this).Create(breakPeriod.StartTime); + } + + foreach (var hitObject in context.Beatmap.HitObjects) + { + foreach (var breakPeriod in context.Beatmap.Breaks) + { + double diffStart = breakPeriod.StartTime - hitObject.GetEndTime(); + double diffEnd = hitObject.StartTime - breakPeriod.EndTime; + + if (diffStart < min_start_threshold - leniency_threshold && diffStart > 0) + yield return new IssueTemplateEarlyStart(this).Create(breakPeriod.StartTime, min_start_threshold - diffStart); + else if (diffEnd < min_end_threshold - leniency_threshold && diffEnd > 0) + yield return new IssueTemplateLateEnd(this).Create(breakPeriod.StartTime, min_end_threshold - diffEnd); + } + } + } + + public class IssueTemplateEarlyStart : IssueTemplate + { + public IssueTemplateEarlyStart(ICheck check) + : base(check, IssueType.Problem, "Break starts {0} ms early.") + { + } + + public Issue Create(double startTime, double diff) => new Issue(startTime, this, (int)diff); + } + + public class IssueTemplateLateEnd : IssueTemplate + { + public IssueTemplateLateEnd(ICheck check) + : base(check, IssueType.Problem, "Break ends {0} ms late.") + { + } + + public Issue Create(double startTime, double diff) => new Issue(startTime, this, (int)diff); + } + + public class IssueTemplateTooShort : IssueTemplate + { + public IssueTemplateTooShort(ICheck check) + : base(check, IssueType.Warning, "Break is non-functional due to being less than 650ms.") + { + } + + public Issue Create(double startTime) => new Issue(startTime, this); + } + } +} From b3974b34e7cd519d6576b0f01c8788976789f35c Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Sun, 16 Jul 2023 23:03:30 +0300 Subject: [PATCH 0734/2100] Test break checks --- .../Editing/Checks/CheckBreaksTest.cs | 105 +++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs b/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs index 664f72c5f8..39e414827a 100644 --- a/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs @@ -1,10 +1,113 @@ // 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 NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; + namespace osu.Game.Tests.Editing.Checks { public class CheckBreaksTest { - + private CheckBreaks check = null!; + + [SetUp] + public void Setup() + { + check = new CheckBreaks(); + } + + [Test] + public void TestBreakTooShort() + { + var beatmap = new Beatmap + { + Breaks = new List + { + new BreakPeriod(0, 649) + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateTooShort); + } + + [Test] + public void TestBreakStartsEarly() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 1_200 } + }, + Breaks = new List + { + new BreakPeriod(100, 751) + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateEarlyStart); + } + + [Test] + public void TestBreakEndsLate() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 1_298 } + }, + Breaks = new List + { + new BreakPeriod(200, 850) + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateLateEnd); + } + + [Test] + public void TestBreaksCorrect() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 1_300 } + }, + Breaks = new List + { + new BreakPeriod(200, 850) + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Is.Empty); + } } } From 17aac0694eec257217bb8f546806d9021cb97147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jul 2023 19:19:03 +0200 Subject: [PATCH 0735/2100] Re-enable connection retrying on discord connector --- osu.Desktop/DiscordRichPresence.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index b1e11d7a60..caf0a1d9fd 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -54,9 +54,6 @@ namespace osu.Desktop client.OnReady += onReady; - // safety measure for now, until we performance test / improve backoff for failed connections. - client.OnConnectionFailed += (_, _) => client.Deinitialize(); - client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network); config.BindWith(OsuSetting.DiscordRichPresence, privacyMode); From ff529d9df7d66dc3191c6f75b3691df451c7aeb9 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Mon, 17 Jul 2023 20:48:53 +0300 Subject: [PATCH 0736/2100] Rename variables, fix check message formatting --- osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs b/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs index 12dc5554f4..54dfb557fe 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Edit.Checks { // Breaks may be off by 1 ms. private const int leniency_threshold = 1; - private const double min_start_threshold = 200; + private const double minimum_gap_before_break = 200; // Break end time depends on the upcoming object's pre-empt time. // As things stand, "pre-empt time" is only defined for osu! standard @@ -40,13 +40,13 @@ namespace osu.Game.Rulesets.Edit.Checks { foreach (var breakPeriod in context.Beatmap.Breaks) { - double diffStart = breakPeriod.StartTime - hitObject.GetEndTime(); - double diffEnd = hitObject.StartTime - breakPeriod.EndTime; + double gapBeforeBreak = breakPeriod.StartTime - hitObject.GetEndTime(); + double gapAfterBreak = hitObject.StartTime - breakPeriod.EndTime; - if (diffStart < min_start_threshold - leniency_threshold && diffStart > 0) - yield return new IssueTemplateEarlyStart(this).Create(breakPeriod.StartTime, min_start_threshold - diffStart); - else if (diffEnd < min_end_threshold - leniency_threshold && diffEnd > 0) - yield return new IssueTemplateLateEnd(this).Create(breakPeriod.StartTime, min_end_threshold - diffEnd); + if (gapBeforeBreak < minimum_gap_before_break - leniency_threshold && gapBeforeBreak > 0) + yield return new IssueTemplateEarlyStart(this).Create(breakPeriod.StartTime, minimum_gap_before_break - gapBeforeBreak); + else if (gapAfterBreak < min_end_threshold - leniency_threshold && gapAfterBreak > 0) + yield return new IssueTemplateLateEnd(this).Create(breakPeriod.StartTime, min_end_threshold - gapAfterBreak); } } } @@ -74,11 +74,11 @@ namespace osu.Game.Rulesets.Edit.Checks public class IssueTemplateTooShort : IssueTemplate { public IssueTemplateTooShort(ICheck check) - : base(check, IssueType.Warning, "Break is non-functional due to being less than 650ms.") + : base(check, IssueType.Warning, "Break is non-functional due to being less than {0} ms.") { } - public Issue Create(double startTime) => new Issue(startTime, this); + public Issue Create(double startTime) => new Issue(startTime, this, BreakPeriod.MIN_BREAK_DURATION); } } } From 768d7b5e1c30329280447dc452fde063b6e440b3 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Mon, 17 Jul 2023 23:31:21 -0400 Subject: [PATCH 0737/2100] correct implementation of stable notelock --- .../TestSceneHitCircle.cs | 2 +- .../Objects/Drawables/DrawableHitCircle.cs | 7 ++- .../Objects/Drawables/DrawableOsuHitObject.cs | 10 ++-- .../Objects/Drawables/DrawableSliderHead.cs | 3 +- osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs | 2 +- osu.Game.Rulesets.Osu/UI/ClickAction.cs | 18 +++++++ osu.Game.Rulesets.Osu/UI/IHitPolicy.cs | 2 +- .../UI/ObjectOrderedHitPolicy.cs | 53 ++++++++++--------- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 2 +- .../UI/StartTimeOrderedHitPolicy.cs | 8 +-- osu.Game/Rulesets/UI/HitObjectContainer.cs | 2 +- osu.Game/Rulesets/UI/IHitObjectContainer.cs | 2 +- 12 files changed, 72 insertions(+), 39 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/UI/ClickAction.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index 0314afc1ac..c818a361df 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Tests protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current) != false) + if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current) == ClickAction.Hit) { // force success ApplyResult(r => r.Type = HitResult.Great); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 3458069dd1..09d818def8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osuTK; @@ -154,13 +155,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } var result = ResultFor(timeOffset); + var clickAction = CheckHittable?.Invoke(this, Time.Current); - if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false) + if (clickAction == ClickAction.Shake || (result == HitResult.None && clickAction != ClickAction.Ignore)) { Shake(); return; } + if (result == HitResult.None) + return; + ApplyResult(r => { var circleResult = (OsuHitCircleJudgementResult)r; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index df0ba344d8..a8ce2118c8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Osu.UI; using osuTK; using osuTK.Graphics; @@ -30,10 +31,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override float SamplePlaybackPosition => CalculateDrawableRelativePosition(this); /// - /// Whether this can be hit, given a time value. - /// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false. + /// What action this should take in response to a + /// click at the given time value. + /// If non-null, judgements will be ignored for return values of + /// and , and this hit object will be shaken for return values of + /// . /// - public Func CheckHittable; + public Func CheckHittable; protected DrawableOsuHitObject(OsuHitObject hitObject) : base(hitObject) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index b8a1efabe0..a4cf69ee31 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -60,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables pathVersion.BindTo(DrawableSlider.PathVersion); - CheckHittable = (d, t) => DrawableSlider.CheckHittable?.Invoke(d, t) ?? true; + CheckHittable = (d, t) => DrawableSlider.CheckHittable?.Invoke(d, t) ?? ClickAction.Hit; } protected override void Update() diff --git a/osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs index afa54c2dfb..7503c43e0b 100644 --- a/osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.UI { public IHitObjectContainer HitObjectContainer { get; set; } - public bool IsHittable(DrawableHitObject hitObject, double time) => true; + public ClickAction CheckHittable(DrawableHitObject hitObject, double time) => ClickAction.Hit; public void HandleHit(DrawableHitObject hitObject) { diff --git a/osu.Game.Rulesets.Osu/UI/ClickAction.cs b/osu.Game.Rulesets.Osu/UI/ClickAction.cs new file mode 100644 index 0000000000..2b00f5acce --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ClickAction.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.UI +{ + /// + /// An action that an recommends be taken in response to a click + /// on a . + /// + public enum ClickAction + { + Ignore, + Shake, + Hit + } +} diff --git a/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs index b509796742..9820b8c188 100644 --- a/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.UI /// The to check. /// The time to check. /// Whether can be hit at the given . - bool IsHittable(DrawableHitObject hitObject, double time); + ClickAction CheckHittable(DrawableHitObject hitObject, double time); /// /// Handles a being hit. diff --git a/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs index 6330208d37..07942954e1 100644 --- a/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs @@ -3,8 +3,7 @@ #nullable disable -using System.Collections.Generic; -using System.Linq; +using System; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -22,35 +21,41 @@ namespace osu.Game.Rulesets.Osu.UI { public IHitObjectContainer HitObjectContainer { get; set; } - public bool IsHittable(DrawableHitObject hitObject, double time) => enumerateHitObjectsUpTo(hitObject.HitObject.StartTime).All(obj => obj.AllJudged); - public void HandleHit(DrawableHitObject hitObject) { } - private IEnumerable enumerateHitObjectsUpTo(double targetTime) + public ClickAction CheckHittable(DrawableHitObject hitObject, double time) { - foreach (var obj in HitObjectContainer.AliveObjects) + int index = HitObjectContainer.AliveObjects.IndexOf(hitObject); + + if (index > 0) { - if (obj.HitObject.StartTime >= targetTime) - yield break; - - switch (obj) - { - case DrawableSpinner: - continue; - - case DrawableSlider slider: - yield return slider.HeadCircle; - - break; - - default: - yield return obj; - - break; - } + var previousHitObject = (DrawableOsuHitObject)HitObjectContainer.AliveObjects[index - 1]; + if (previousHitObject.HitObject.StackHeight > 0 && !previousHitObject.AllJudged) + return ClickAction.Ignore; } + + foreach (DrawableHitObject testObject in HitObjectContainer.AliveObjects) + { + if (testObject.AllJudged) + continue; + + // if we found the object being checked, we can move on to the final timing test. + if (testObject == hitObject) + break; + + // for all other objects, we check for validity and block the hit if any are still valid. + // 3ms of extra leniency to account for slightly unsnapped objects. + if (testObject.HitObject.GetEndTime() + 3 < hitObject.HitObject.StartTime) + return ClickAction.Shake; + } + + // stable has `const HitObjectManager.HITTABLE_RANGE = 400;`, which is only used for notelock code. + // probably not a coincidence that this is equivalent to lazer's OsuHitWindows.MISS_WINDOW. + + // TODO stable compares to 200 when autopilot is enabled, instead of 400. + return Math.Abs(hitObject.HitObject.StartTime - time) < 400 ? ClickAction.Hit : ClickAction.Shake; } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index ed02284a4b..15ca0a90de 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override void OnNewDrawableHitObject(DrawableHitObject drawable) { - ((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable; + ((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.CheckHittable; Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}"); drawable.OnLoadComplete += onDrawableHitObjectLoaded; diff --git a/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs index edc3ba0818..f33ca58aef 100644 --- a/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.UI { public IHitObjectContainer HitObjectContainer { get; set; } - public bool IsHittable(DrawableHitObject hitObject, double time) + public ClickAction CheckHittable(DrawableHitObject hitObject, double time) { DrawableHitObject blockingObject = null; @@ -36,13 +36,13 @@ namespace osu.Game.Rulesets.Osu.UI // If there is no previous hitobject, allow the hit. if (blockingObject == null) - return true; + return ClickAction.Hit; // A hit is allowed if: // 1. The last blocking hitobject has been judged. // 2. The current time is after the last hitobject's start time. // Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245). - return blockingObject.Judged || time >= blockingObject.HitObject.StartTime; + return (blockingObject.Judged || time >= blockingObject.HitObject.StartTime) ? ClickAction.Hit : ClickAction.Shake; } public void HandleHit(DrawableHitObject hitObject) @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.UI if (!hitObjectCanBlockFutureHits(hitObject)) return; - if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) + if (CheckHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset) != ClickAction.Hit) throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!"); // Miss all hitobjects prior to the hit one. diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 099be486b3..454a83bcda 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.UI { public IEnumerable Objects => InternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); - public IEnumerable AliveObjects => AliveEntries.Select(pair => pair.Drawable).OrderBy(h => h.HitObject.StartTime); + public IList AliveObjects => AliveEntries.Select(pair => pair.Drawable).OrderBy(h => h.HitObject.StartTime).ToList(); /// /// Invoked when a is judged. diff --git a/osu.Game/Rulesets/UI/IHitObjectContainer.cs b/osu.Game/Rulesets/UI/IHitObjectContainer.cs index 6dcb0944be..bb4806206d 100644 --- a/osu.Game/Rulesets/UI/IHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/IHitObjectContainer.cs @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.UI /// /// If this uses pooled objects, this is equivalent to . /// - IEnumerable AliveObjects { get; } + IList AliveObjects { get; } } } From e6e66c6aefa197732de8b0d425cc736870332147 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jul 2023 13:08:23 +0900 Subject: [PATCH 0738/2100] Remove mention of clock being nullable in `IBeatSyncProvider` Co-authored-by: Susko3 --- osu.Game/Beatmaps/IBeatSyncProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/IBeatSyncProvider.cs b/osu.Game/Beatmaps/IBeatSyncProvider.cs index 776552cfa5..61fcf7f8e2 100644 --- a/osu.Game/Beatmaps/IBeatSyncProvider.cs +++ b/osu.Game/Beatmaps/IBeatSyncProvider.cs @@ -22,7 +22,7 @@ namespace osu.Game.Beatmaps ControlPointInfo? ControlPoints { get; } /// - /// Access a clock currently responsible for providing beat sync. If null, no current provider is available. + /// Access a clock currently responsible for providing beat sync. /// IClock Clock { get; } } From 49bb0b190a6531a8f65bccec747747ae31858f27 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jul 2023 16:14:54 +0900 Subject: [PATCH 0739/2100] Split out star constant to reuse in pool definition --- osu.Game/Screens/Menu/StarFountain.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs index 7fa996e39a..3cfa529d40 100644 --- a/osu.Game/Screens/Menu/StarFountain.cs +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -13,6 +13,8 @@ namespace osu.Game.Screens.Menu { public partial class StarFountain : CompositeDrawable { + private const int stars_per_shoot = 192; + private DrawablePool starPool = null!; private Container starContainer = null!; @@ -21,7 +23,7 @@ namespace osu.Game.Screens.Menu { InternalChildren = new Drawable[] { - starPool = new DrawablePool(192), + starPool = new DrawablePool(stars_per_shoot), starContainer = new Container() }; } @@ -31,8 +33,6 @@ namespace osu.Game.Screens.Menu // left centre or right movement. int direction = RNG.Next(-1, 2); - const int total_stars = 192; - const float x_velocity_from_direction = 0.6f; const float x_velocity_random_variance = 0.25f; @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Menu const float x_spawn_position_variance = 10; const float y_spawn_position_offset = 50; - for (int i = 0; i < total_stars; i++) + for (int i = 0; i < stars_per_shoot; i++) { double initialOffset = i * 3; From f4acc86df8ad3ea0d36b78bc2a7603322f954a36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jul 2023 16:26:37 +0900 Subject: [PATCH 0740/2100] Adjust metrics to closer match expectations --- osu.Game/Screens/Menu/StarFountain.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs index 3cfa529d40..3b4fdd7abf 100644 --- a/osu.Game/Screens/Menu/StarFountain.cs +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -33,30 +33,30 @@ namespace osu.Game.Screens.Menu // left centre or right movement. int direction = RNG.Next(-1, 2); - const float x_velocity_from_direction = 0.6f; - const float x_velocity_random_variance = 0.25f; + const float x_velocity_from_direction = 0.8f; + const float x_velocity_random_variance = 0.15f; const float y_velocity_base = -2.0f; - const float y_velocity_random_variance = 0.25f; + const float y_velocity_random_variance = 0.15f; const float x_spawn_position_variance = 10; const float y_spawn_position_offset = 50; for (int i = 0; i < stars_per_shoot; i++) { - double initialOffset = i * 3; + double capturedIndex = i; starContainer.Add(starPool.Get(s => { s.Velocity = new Vector2( - direction * x_velocity_from_direction + getRandomVariance(x_velocity_random_variance), + direction * x_velocity_from_direction * (1 - 2 * ((float)capturedIndex / stars_per_shoot)) + getRandomVariance(x_velocity_random_variance), y_velocity_base + getRandomVariance(y_velocity_random_variance)); s.Position = new Vector2(getRandomVariance(x_spawn_position_variance), y_spawn_position_offset); s.Hide(); - using (s.BeginDelayedSequence(initialOffset)) + using (s.BeginDelayedSequence(capturedIndex * 3)) { double duration = RNG.Next(300, 1300); From c02684d985e426b8dab1bf175204026cff2845e3 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 18 Jul 2023 12:18:43 +0200 Subject: [PATCH 0741/2100] truncate hit object end time --- osu.Game/Database/LegacyBeatmapExporter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 42d8a72073..3b2282c234 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -60,6 +60,9 @@ namespace osu.Game.Database { hitObject.StartTime = Math.Floor(hitObject.StartTime); + if (hitObject is IHasDuration hasDuration && hitObject is not IHasPath) + hasDuration.Duration = Math.Floor(hasDuration.Duration); + if (hitObject is not IHasPath hasPath || BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue; var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); From bcdf5310390021d2b5b4996c6d88293846a12004 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 18 Jul 2023 12:28:35 +0200 Subject: [PATCH 0742/2100] truncate end time before start time --- osu.Game/Database/LegacyBeatmapExporter.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 3b2282c234..983d25a25a 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -58,10 +58,11 @@ namespace osu.Game.Database foreach (var hitObject in beatmapContent.HitObjects) { - hitObject.StartTime = Math.Floor(hitObject.StartTime); - + // Truncate end time before truncating start time because end time is dependent on start time if (hitObject is IHasDuration hasDuration && hitObject is not IHasPath) - hasDuration.Duration = Math.Floor(hasDuration.Duration); + hasDuration.Duration = Math.Floor(hasDuration.EndTime) - Math.Floor(hitObject.StartTime); + + hitObject.StartTime = Math.Floor(hitObject.StartTime); if (hitObject is not IHasPath hasPath || BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue; From 395dd23966d0ae3fb15b13b8e0b8c6628e1a310b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 18 Jul 2023 12:37:37 +0200 Subject: [PATCH 0743/2100] Put 'Export package' and 'Export legacy package' in one nested menu --- osu.Game/Localisation/EditorStrings.cs | 9 +++++++-- osu.Game/Screens/Edit/Editor.cs | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 70392d5c83..07b4dddc05 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -40,9 +40,14 @@ namespace osu.Game.Localisation public static LocalisableString ExportPackage => new TranslatableString(getKey(@"export_package"), @"Export package"); /// - /// "Export legacy package" + /// "Legacy format (.osz)" /// - public static LocalisableString ExportLegacyPackage => new TranslatableString(getKey(@"export_legacy_package"), @"Export legacy package"); + public static LocalisableString ExportLegacyFormat => new TranslatableString(getKey(@"export_legacy_format"), @"Legacy format (.osz)"); + + /// + /// "New format (.olz)" + /// + public static LocalisableString ExportNewFormat => new TranslatableString(getKey(@"export_new_format"), @"New format (.olz)"); /// /// "Create new difficulty" diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a48c57f991..16cd7768fa 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -998,8 +998,7 @@ namespace osu.Game.Screens.Edit private List createFileMenuItems() => new List { new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), - new EditorMenuItem(EditorStrings.ExportPackage, MenuItemType.Standard, exportBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, - new EditorMenuItem(EditorStrings.ExportLegacyPackage, MenuItemType.Standard, exportLegacyBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + createExportPackageMenu(), new EditorMenuItemSpacer(), createDifficultyCreationMenu(), createDifficultySwitchMenu(), @@ -1009,6 +1008,17 @@ namespace osu.Game.Screens.Edit new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit) }; + private EditorMenuItem createExportPackageMenu() + { + var exportItems = new List + { + new EditorMenuItem(EditorStrings.ExportNewFormat, MenuItemType.Standard, exportBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + new EditorMenuItem(EditorStrings.ExportLegacyFormat, MenuItemType.Standard, exportLegacyBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + }; + + return new EditorMenuItem(EditorStrings.ExportPackage) { Items = exportItems }; + } + private void exportBeatmap() { Save(); From 63dd8bd991835344cbbf3e0c88b8f4b749ea1029 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 18 Jul 2023 12:40:48 +0200 Subject: [PATCH 0744/2100] use base.GetFileContents to get file stream --- osu.Game/Database/LegacyBeatmapExporter.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 983d25a25a..b90ea73244 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -8,7 +8,6 @@ using System.Text; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; -using osu.Game.Extensions; using osu.Game.IO; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -36,7 +35,7 @@ namespace osu.Game.Database return base.GetFileContents(model, file); // Read the beatmap contents and skin - using var contentStream = UserFileStorage.GetStream(file.File.GetStoragePath()); + using var contentStream = base.GetFileContents(model, file); if (contentStream == null) return null; @@ -44,7 +43,7 @@ namespace osu.Game.Database using var contentStreamReader = new LineBufferedReader(contentStream); var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader); - using var skinStream = UserFileStorage.GetStream(file.File.GetStoragePath()); + using var skinStream = base.GetFileContents(model, file); using var skinStreamReader = new LineBufferedReader(contentStream); var beatmapSkin = new LegacySkin(new SkinInfo(), null!) { From ccbb30cdda8370ad626b30a56d1d9c7f169d03ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jul 2023 19:52:45 +0900 Subject: [PATCH 0745/2100] Fix `ParticleSpewer` not correctly accounting for lower frame rates (and spawning less particles) --- osu.Game/Graphics/ParticleSpewer.cs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/ParticleSpewer.cs b/osu.Game/Graphics/ParticleSpewer.cs index 8519cf0c59..02053e1be7 100644 --- a/osu.Game/Graphics/ParticleSpewer.cs +++ b/osu.Game/Graphics/ParticleSpewer.cs @@ -20,9 +20,9 @@ namespace osu.Game.Graphics { private readonly FallingParticle[] particles; private int currentIndex; - private double lastParticleAdded; + private double? lastParticleAdded; - private readonly double cooldown; + private readonly double timeBetweenSpawns; private readonly double maxDuration; /// @@ -44,7 +44,7 @@ namespace osu.Game.Graphics particles = new FallingParticle[perSecond * (int)Math.Ceiling(maxDuration / 1000)]; - cooldown = 1000f / perSecond; + timeBetweenSpawns = 1000f / perSecond; this.maxDuration = maxDuration; } @@ -52,18 +52,27 @@ namespace osu.Game.Graphics { base.Update(); - if (Active.Value && CanSpawnParticles && Math.Abs(Time.Current - lastParticleAdded) > cooldown) + Invalidate(Invalidation.DrawNode); + + if (!Active.Value || !CanSpawnParticles) { + lastParticleAdded = null; + return; + } + + while (lastParticleAdded == null || Math.Abs(Time.Current - lastParticleAdded.Value) > timeBetweenSpawns) + { + lastParticleAdded ??= Time.Current; + var newParticle = CreateParticle(); - newParticle.StartTime = (float)Time.Current; + newParticle.StartTime = (float)lastParticleAdded; particles[currentIndex] = newParticle; currentIndex = (currentIndex + 1) % particles.Length; - lastParticleAdded = Time.Current; - } - Invalidate(Invalidation.DrawNode); + lastParticleAdded += timeBetweenSpawns; + } } /// From ba237a0e3aad8e37b4a75f2a0d79afdf51005e58 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jul 2023 19:52:58 +0900 Subject: [PATCH 0746/2100] Convert `StarFountain` to use `ParticleSpewer` More efficient --- osu.Game/Screens/Menu/StarFountain.cs | 129 +++++++++++--------------- 1 file changed, 54 insertions(+), 75 deletions(-) diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs index 3b4fdd7abf..debf0007eb 100644 --- a/osu.Game/Screens/Menu/StarFountain.cs +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -2,113 +2,92 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Textures; using osu.Framework.Utils; +using osu.Game.Graphics; using osu.Game.Skinning; using osuTK; namespace osu.Game.Screens.Menu { - public partial class StarFountain : CompositeDrawable + public partial class StarFountain : SkinReloadableDrawable { - private const int stars_per_shoot = 192; + private StarFountainSpewer spewer = null!; - private DrawablePool starPool = null!; - private Container starContainer = null!; + [Resolved] + private TextureStore textures { get; set; } = null!; [BackgroundDependencyLoader] private void load() { - InternalChildren = new Drawable[] - { - starPool = new DrawablePool(stars_per_shoot), - starContainer = new Container() - }; + InternalChild = spewer = new StarFountainSpewer(); } - public void Shoot() + public void Shoot() => spewer.Shoot(); + + protected override void SkinChanged(ISkinSource skin) { - // left centre or right movement. - int direction = RNG.Next(-1, 2); + base.SkinChanged(skin); + spewer.Texture = skin.GetTexture("Menu/fountain-star") ?? textures.Get("Menu/fountain-star"); + } - const float x_velocity_from_direction = 0.8f; - const float x_velocity_random_variance = 0.15f; + public partial class StarFountainSpewer : ParticleSpewer + { + private const int particle_duration_min = 300; + private const int particle_duration_max = 1000; - const float y_velocity_base = -2.0f; - const float y_velocity_random_variance = 0.15f; + private double? lastShootTime; + private int lastShootDirection; - const float x_spawn_position_variance = 10; - const float y_spawn_position_offset = 50; + protected override float ParticleGravity => 800; - for (int i = 0; i < stars_per_shoot; i++) + private const double shoot_duration = 800; + + protected override bool CanSpawnParticles => lastShootTime != null && Time.Current - lastShootTime < shoot_duration; + + [Resolved] + private ISkinSource skin { get; set; } = null!; + + public StarFountainSpewer() + : base(null, 240, particle_duration_max) { - double capturedIndex = i; - - starContainer.Add(starPool.Get(s => - { - s.Velocity = new Vector2( - direction * x_velocity_from_direction * (1 - 2 * ((float)capturedIndex / stars_per_shoot)) + getRandomVariance(x_velocity_random_variance), - y_velocity_base + getRandomVariance(y_velocity_random_variance)); - - s.Position = new Vector2(getRandomVariance(x_spawn_position_variance), y_spawn_position_offset); - - s.Hide(); - - using (s.BeginDelayedSequence(capturedIndex * 3)) - { - double duration = RNG.Next(300, 1300); - - s.ScaleTo(1) - .ScaleTo(RNG.NextSingle(1, 2.8f), duration, Easing.Out) - .FadeOutFromOne(duration, Easing.Out) - .Expire(); - } - })); } - } - - private partial class Star : PoolableDrawable - { - public Vector2 Velocity = Vector2.Zero; - - private float rotation; [BackgroundDependencyLoader] - private void load() + private void load(TextureStore textures) { - AutoSizeAxes = Axes.Both; - Origin = Anchor.Centre; + Texture = skin.GetTexture("Menu/fountain-star") ?? textures.Get("Menu/fountain-star"); + Active.Value = true; + } - InternalChildren = new Drawable[] + protected override FallingParticle CreateParticle() + { + return new FallingParticle { - new SkinnableSprite("Menu/fountain-star") - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Blending = BlendingParameters.Additive, - } + StartPosition = new Vector2(0, 50), + Duration = RNG.NextSingle(particle_duration_min, particle_duration_max), + StartAngle = getRandomVariance(4), + EndAngle = getRandomVariance(2), + EndScale = 1.4f + getRandomVariance(0.4f), + Velocity = new Vector2(getCurrentAngle(), -1200 + getRandomVariance(100)), }; - - rotation = getRandomVariance(2); } - protected override void Update() + private float getCurrentAngle() { - const float gravity = 0.003f; + const float x_velocity_from_direction = 500; + const float x_velocity_random_variance = 60; - base.Update(); - - float elapsed = (float)Time.Elapsed; - - Position += Velocity * elapsed; - Velocity += new Vector2(0, elapsed * gravity); - - Rotation += rotation * elapsed; + return lastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / shoot_duration) + getRandomVariance(x_velocity_random_variance); } - } - private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance); + public void Shoot() + { + lastShootTime = Clock.CurrentTime; + lastShootDirection = RNG.Next(-1, 2); + } + + private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance); + } } } From e7a9175aeae253a515c8a8ab0d40985853de28ec Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 18 Jul 2023 13:08:05 +0200 Subject: [PATCH 0747/2100] fix skin using wrong stream --- osu.Game/Database/LegacyBeatmapExporter.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index b90ea73244..e054652efa 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -44,7 +44,11 @@ namespace osu.Game.Database var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader); using var skinStream = base.GetFileContents(model, file); - using var skinStreamReader = new LineBufferedReader(contentStream); + + if (skinStream == null) + return null; + + using var skinStreamReader = new LineBufferedReader(skinStream); var beatmapSkin = new LegacySkin(new SkinInfo(), null!) { Configuration = new LegacySkinDecoder().Decode(skinStreamReader) From 2e3d1fe950d292e6764715c892aa9ad0759d94b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 13:38:04 +0900 Subject: [PATCH 0748/2100] Fix regression in time jumping behaviour --- .../Gameplay/TestSceneParticleSpewer.cs | 6 +-- osu.Game/Graphics/ParticleSpewer.cs | 47 ++++++++++++++----- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs index c73d57dc2b..8fb34883bb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs @@ -69,13 +69,13 @@ namespace osu.Game.Tests.Visual.Gameplay spewer.Clock = new FramedClock(testClock); }); AddStep("start spewer", () => spewer.Active.Value = true); - AddAssert("spawned first particle", () => spewer.TotalCreatedParticles == 1); + AddAssert("spawned first particle", () => spewer.TotalCreatedParticles, () => Is.EqualTo(1)); AddStep("move clock forward", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * 3); - AddAssert("spawned second particle", () => spewer.TotalCreatedParticles == 2); + AddAssert("spawned second particle", () => spewer.TotalCreatedParticles, () => Is.EqualTo(2)); AddStep("move clock backwards", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * -1); - AddAssert("spawned third particle", () => spewer.TotalCreatedParticles == 3); + AddAssert("spawned third particle", () => spewer.TotalCreatedParticles, () => Is.EqualTo(3)); } private TestParticleSpewer createSpewer() => diff --git a/osu.Game/Graphics/ParticleSpewer.cs b/osu.Game/Graphics/ParticleSpewer.cs index 02053e1be7..c761ec0b7e 100644 --- a/osu.Game/Graphics/ParticleSpewer.cs +++ b/osu.Game/Graphics/ParticleSpewer.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Diagnostics; using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; @@ -60,19 +61,43 @@ namespace osu.Game.Graphics return; } - while (lastParticleAdded == null || Math.Abs(Time.Current - lastParticleAdded.Value) > timeBetweenSpawns) + // Always want to spawn the first particle in an activation immediately. + if (lastParticleAdded == null) { - lastParticleAdded ??= Time.Current; - - var newParticle = CreateParticle(); - newParticle.StartTime = (float)lastParticleAdded; - - particles[currentIndex] = newParticle; - - currentIndex = (currentIndex + 1) % particles.Length; - - lastParticleAdded += timeBetweenSpawns; + lastParticleAdded = Time.Current; + spawnParticle(); } + + double timeElapsed = Math.Abs(Time.Current - lastParticleAdded.Value); + + // Avoid spawning too many particles if a long amount of time has passed. + if (timeElapsed > maxDuration) + { + lastParticleAdded = Time.Current; + spawnParticle(); + return; + } + + Debug.Assert(lastParticleAdded != null); + + for (int i = 0; i < timeElapsed / timeBetweenSpawns; i++) + { + lastParticleAdded += timeBetweenSpawns; + spawnParticle(); + } + } + + private void spawnParticle() + { + Debug.Assert(lastParticleAdded != null); + + var newParticle = CreateParticle(); + + newParticle.StartTime = (float)lastParticleAdded.Value; + + particles[currentIndex] = newParticle; + + currentIndex = (currentIndex + 1) % particles.Length; } /// From 7fa95f7512f8b7737c1d569dcb2158b73939c89c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 13:52:43 +0900 Subject: [PATCH 0749/2100] Fix `#region` --- osu.Game/Graphics/ParticleSpewer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/ParticleSpewer.cs b/osu.Game/Graphics/ParticleSpewer.cs index c761ec0b7e..9cf3fb1882 100644 --- a/osu.Game/Graphics/ParticleSpewer.cs +++ b/osu.Game/Graphics/ParticleSpewer.cs @@ -107,7 +107,7 @@ namespace osu.Game.Graphics protected override DrawNode CreateDrawNode() => new ParticleSpewerDrawNode(this); - # region DrawNode + #region DrawNode private class ParticleSpewerDrawNode : SpriteDrawNode { From 5e2a0bd73302f2f6be74bb9792d0e067f1c092ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 14:10:27 +0900 Subject: [PATCH 0750/2100] Fix spawning edge case when elapsed became unexpectedly negative --- osu.Game/Graphics/ParticleSpewer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/ParticleSpewer.cs b/osu.Game/Graphics/ParticleSpewer.cs index 9cf3fb1882..37a4fe77bd 100644 --- a/osu.Game/Graphics/ParticleSpewer.cs +++ b/osu.Game/Graphics/ParticleSpewer.cs @@ -66,12 +66,13 @@ namespace osu.Game.Graphics { lastParticleAdded = Time.Current; spawnParticle(); + return; } - double timeElapsed = Math.Abs(Time.Current - lastParticleAdded.Value); + double timeElapsed = Time.Current - lastParticleAdded.Value; // Avoid spawning too many particles if a long amount of time has passed. - if (timeElapsed > maxDuration) + if (Math.Abs(timeElapsed) > maxDuration) { lastParticleAdded = Time.Current; spawnParticle(); From b9a66ad7b346aa26d49a875a522deb8932b8dca9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 14:58:45 +0900 Subject: [PATCH 0751/2100] Add test coverage of incorrect selection behaviour --- .../Editor/TestSceneOsuComposerSelection.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs index 8641663ce8..623cefff6b 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs @@ -25,6 +25,35 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + [Test] + public void TestSelectAfterFadedOut() + { + var slider = new Slider + { + StartTime = 0, + Position = new Vector2(100, 100), + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint(), + new PathControlPoint(new Vector2(100)) + } + } + }; + AddStep("add slider", () => EditorBeatmap.Add(slider)); + + moveMouseToObject(() => slider); + + AddStep("seek after end", () => EditorClock.Seek(750)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("slider not selected", () => EditorBeatmap.SelectedHitObjects.Count == 0); + + AddStep("seek to visible", () => EditorClock.Seek(650)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddUntilStep("slider selected", () => EditorBeatmap.SelectedHitObjects.Single() == slider); + } + [Test] public void TestContextMenuShownCorrectlyForSelectedSlider() { From 4a6a5b174a3255b8479edb2ab4683fd779040925 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 14:52:31 +0900 Subject: [PATCH 0752/2100] Fix editor blueprints being selectable for too long when hit markers are enabled Addresses https://github.com/ppy/osu/discussions/24163. --- .../Edit/Blueprints/OsuSelectionBlueprint.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs index bdd19f9106..178b809d8b 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs @@ -22,7 +22,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints protected override bool AlwaysShowWhenSelected => true; protected override bool ShouldBeAlive => base.ShouldBeAlive - || (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION); + || (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime + && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION); + + public override bool HandlePositionalInput => + // Bypass fade out extension from hit markers for input handling purposes. + // This is to match stable, where even when the afterimage hit markers are still visible, objects are not selectable. + // + // Note that we are intentionally overriding HandlePositionalInput here and not ReceivePositionalInputAt + // as individual blueprint implementations override that. + base.ShouldBeAlive; protected OsuSelectionBlueprint(T hitObject) : base(hitObject) From 871056790b4bfb56fb5d5fad4e3e224673b53987 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 15:01:20 +0900 Subject: [PATCH 0753/2100] Mark editor tile as non-localisable --- osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index fa4e52d6a6..e958849bb0 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -61,8 +61,8 @@ namespace osu.Game.Screens.Edit.Components.Menus }, }); - text.AddText("osu!", t => t.Font = OsuFont.TorusAlternate); - text.AddText("editor", t => + text.AddText(@"osu!", t => t.Font = OsuFont.TorusAlternate); + text.AddText(@"editor", t => { t.Font = OsuFont.TorusAlternate; t.Colour = colourProvider.Highlight1; From 15af85226ce9c15c6bdb18d1c727cd257683d4e1 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Wed, 19 Jul 2023 02:06:29 -0400 Subject: [PATCH 0754/2100] adjust test for correct stable notelock stable actually allows for hitobjs to be hit in the middle of sliders, as long as it doesn't interfere with the end time of the slider. --- .../TestSceneObjectOrderedHitPolicy.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index ee70441688..be2affa50f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -213,10 +213,10 @@ namespace osu.Game.Rulesets.Osu.Tests } /// - /// Tests clicking a future circle after a slider's start time, but hitting all slider ticks. + /// Tests clicking a future circle after a slider's start time, but hitting the slider head and all slider ticks. /// [Test] - public void TestMissSliderHeadAndHitAllSliderTicks() + public void TestHitCircleBeforeSliderHead() { const double time_slider = 1500; const double time_circle = 1510; @@ -248,7 +248,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } } }); - addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[0], HitResult.Great); addJudgementAssert(hitObjects[1], HitResult.Great); addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit); addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); From 55a41b0887ff6e7d78502f40dc61204eda4c0306 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 15:13:20 +0900 Subject: [PATCH 0755/2100] Fix overlap between header text and menu items --- osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index e958849bb0..b9385ff0c3 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -25,7 +25,10 @@ namespace osu.Game.Screens.Edit.Components.Menus RelativeSizeAxes = Axes.X; MaskingContainer.CornerRadius = 0; - ItemsContainer.Padding = new MarginPadding { Left = heading_area }; + ItemsContainer.Padding = new MarginPadding(); + + ContentContainer.Margin = new MarginPadding { Left = heading_area }; + ContentContainer.Masking = true; } [BackgroundDependencyLoader] From 4e4dcc9846cdb1c9c052b021d3d90681ce877bff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 16:32:04 +0900 Subject: [PATCH 0756/2100] Add test coverage of selection preferring closest objects --- .../Editing/TestSceneComposerSelection.cs | 75 ++++++++++++++++++- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index d6934a3770..be5dd59206 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -6,21 +6,21 @@ using System; using System.Linq; using NUnit.Framework; -using osu.Framework.Testing; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; -using osu.Game.Tests.Beatmaps; using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Tests.Beatmaps; using osuTK; using osuTK.Input; @@ -217,6 +217,75 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1])); } + [Test] + public void TestNearestSelection() + { + var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 }; + var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject })); + + moveMouseToObject(() => firstObject); + + AddStep("seek near first", () => EditorClock.Seek(100)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + + AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear()); + + AddStep("seek near second", () => EditorClock.Seek(500)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + + AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear()); + + AddStep("seek halfway", () => EditorClock.Seek(300)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + } + + [Test] + public void TestNearestSelectionWithEndTime() + { + var firstObject = new Slider + { + Position = new Vector2(256, 192), + StartTime = 0, + Path = new SliderPath(new[] + { + new PathControlPoint(), + new PathControlPoint(new Vector2(50, 0)), + }) + }; + + var secondObject = new HitCircle + { + Position = new Vector2(256, 192), + StartTime = 600 + }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new HitObject[] { firstObject, secondObject })); + + moveMouseToObject(() => firstObject); + + AddStep("seek near first", () => EditorClock.Seek(100)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + + AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear()); + + AddStep("seek near second", () => EditorClock.Seek(500)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + + AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear()); + + AddStep("seek roughly halfway", () => EditorClock.Seek(350)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + // Slider gets priority due to end time. + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + } + [TestCase(false)] [TestCase(true)] public void TestMultiSelectFromDrag(bool alreadySelectedBeforeDrag) From 5ade093c5a70f32971ed26422c7c53f29b6529bd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 15:55:38 +0900 Subject: [PATCH 0757/2100] Change editor to always perform selection of closest object --- .../Compose/Components/BlueprintContainer.cs | 27 ++++++++++++++----- .../Components/EditorBlueprintContainer.cs | 5 ++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 56a6b18433..5fdd2634c4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -381,6 +381,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// private bool selectedBlueprintAlreadySelectedOnMouseDown; + protected virtual IEnumerable> ApplySelectionOrder(IEnumerable> blueprints) => blueprints.Reverse(); + /// /// Attempts to select any hovered blueprints. /// @@ -390,15 +392,28 @@ namespace osu.Game.Screens.Edit.Compose.Components { // Iterate from the top of the input stack (blueprints closest to the front of the screen first). // Priority is given to already-selected blueprints. - foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected)) + foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Where(b => b.IsSelected)) { - if (!blueprint.IsHovered) continue; + if (runForBlueprint(blueprint)) + return true; + } - selectedBlueprintAlreadySelectedOnMouseDown = blueprint.State == SelectionState.Selected; - return clickSelectionHandled = SelectionHandler.MouseDownSelectionRequested(blueprint, e); + foreach (SelectionBlueprint blueprint in ApplySelectionOrder(SelectionBlueprints.AliveChildren)) + { + if (runForBlueprint(blueprint)) + return true; } return false; + + bool runForBlueprint(SelectionBlueprint blueprint) + { + if (!blueprint.IsHovered) return false; + + selectedBlueprintAlreadySelectedOnMouseDown = blueprint.State == SelectionState.Selected; + clickSelectionHandled = SelectionHandler.MouseDownSelectionRequested(blueprint, e); + return true; + } } /// @@ -432,13 +447,13 @@ namespace osu.Game.Screens.Edit.Compose.Components // The depth of blueprints is constantly changing (see above where selected blueprints are brought to the front). // For this logic, we want a stable sort order so we can correctly cycle, thus using the blueprintMap instead. - IEnumerable> cyclingSelectionBlueprints = blueprintMap.Values; + IEnumerable> cyclingSelectionBlueprints = ApplySelectionOrder(blueprintMap.Values); // If there's already a selection, let's start from the blueprint after the selection. cyclingSelectionBlueprints = cyclingSelectionBlueprints.SkipWhile(b => !b.IsSelected).Skip(1); // Add the blueprints from before the selection to the end of the enumerable to allow for cyclic selection. - cyclingSelectionBlueprints = cyclingSelectionBlueprints.Concat(blueprintMap.Values.TakeWhile(b => !b.IsSelected)); + cyclingSelectionBlueprints = cyclingSelectionBlueprints.Concat(ApplySelectionOrder(blueprintMap.Values).TakeWhile(b => !b.IsSelected)); foreach (SelectionBlueprint blueprint in cyclingSelectionBlueprints) { diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index 65797a968d..ad0e8b124b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -129,6 +130,10 @@ namespace osu.Game.Screens.Edit.Compose.Components return true; } + protected override IEnumerable> ApplySelectionOrder(IEnumerable> blueprints) => + base.ApplySelectionOrder(blueprints) + .OrderBy(b => Math.Min(Math.Abs(EditorClock.CurrentTime - b.Item.GetEndTime()), Math.Abs(EditorClock.CurrentTime - b.Item.StartTime))); + protected override Container> CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }; protected override SelectionHandler CreateSelectionHandler() => new EditorSelectionHandler(); From 9d46d00294cda3ea10fca9507d008975efcc91d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 16:17:42 +0900 Subject: [PATCH 0758/2100] Update skin editor cyclic test to match expectations better --- .../Visual/Gameplay/TestSceneSkinEditor.cs | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 7b37b6624d..4e5db5d46e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -138,24 +138,28 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestCyclicSelection() { - SkinBlueprint[] blueprints = null!; + List blueprints = new List(); - AddStep("Add big black boxes", () => + AddStep("clear list", () => blueprints.Clear()); + + for (int i = 0; i < 3; i++) { - InputManager.MoveMouseTo(skinEditor.ChildrenOfType().First()); - InputManager.Click(MouseButton.Left); - InputManager.Click(MouseButton.Left); - InputManager.Click(MouseButton.Left); - }); + AddStep("Add big black box", () => + { + InputManager.MoveMouseTo(skinEditor.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + + AddStep("store box", () => + { + // Add blueprints one-by-one so we have a stable order for testing reverse cyclic selection against. + blueprints.Add(skinEditor.ChildrenOfType().Single(s => s.IsSelected)); + }); + } AddAssert("Three black boxes added", () => targetContainer.Components.OfType().Count(), () => Is.EqualTo(3)); - AddStep("Store black box blueprints", () => - { - blueprints = skinEditor.ChildrenOfType().Where(b => b.Item is BigBlackBox).ToArray(); - }); - - AddAssert("Selection is black box 1", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item)); + AddAssert("Selection is last", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item)); AddStep("move cursor to black box", () => { @@ -164,13 +168,13 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left)); - AddAssert("Selection is black box 2", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[1].Item)); + AddAssert("Selection is second last", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[1].Item)); AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left)); - AddAssert("Selection is black box 3", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item)); + AddAssert("Selection is last", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item)); AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left)); - AddAssert("Selection is black box 1", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item)); + AddAssert("Selection is first", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item)); AddStep("select all boxes", () => { From 5ec9cd84b25631ba0bc8ed2c0efcadc331a60afe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 17:13:19 +0900 Subject: [PATCH 0759/2100] Change offset calibration control to adjust for all difficulties of the current beatmap set --- .../Play/PlayerSettings/BeatmapOffsetControl.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index b542707185..840077eb7f 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -162,17 +162,20 @@ namespace osu.Game.Screens.Play.PlayerSettings realmWriteTask = realm.WriteAsync(r => { - var settings = r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings; + var setInfo = r.Find(beatmap.Value.BeatmapSetInfo.ID); - if (settings == null) // only the case for tests. + if (setInfo == null) // only the case for tests. return; - double val = Current.Value; + // Apply to all difficulties in a beatmap set for now (they generally always share timing). + foreach (var b in setInfo.Beatmaps) + { + BeatmapUserSettings settings = b.UserSettings; + double val = Current.Value; - if (settings.Offset == val) - return; - - settings.Offset = val; + if (settings.Offset != val) + settings.Offset = val; + } }); } } From d33b174243ef6fe98b3c9ac64cfb6f4ccc91d91c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 17:21:17 +0900 Subject: [PATCH 0760/2100] Add test coverage of beatmap editor cyclic selection --- .../Editing/TestSceneComposerSelection.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index be5dd59206..d7fb13390f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -286,6 +286,85 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); } + [Test] + public void TestCyclicSelection() + { + var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 }; + var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 300 }; + var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject })); + + moveMouseToObject(() => firstObject); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject)); + + // cycle around + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + } + + [Test] + public void TestCyclicSelectionOutwards() + { + var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 }; + var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 300 }; + var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject })); + + moveMouseToObject(() => firstObject); + + AddStep("seek near first", () => EditorClock.Seek(320)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + + // cycle around + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + } + + [Test] + public void TestCyclicSelectionBackwards() + { + var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 }; + var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 300 }; + var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject })); + + moveMouseToObject(() => firstObject); + + AddStep("seek near first", () => EditorClock.Seek(600)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + + // cycle around + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject)); + } + [TestCase(false)] [TestCase(true)] public void TestMultiSelectFromDrag(bool alreadySelectedBeforeDrag) From cf3949c9e21b3fe4f085a8a43d56b60ecdde5ae6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 15:35:40 +0900 Subject: [PATCH 0761/2100] Fix double-click handling when cyclic selection is enabled Removes the limitations of cyclic selection as a result. --- .../SkinEditor/SkinBlueprintContainer.cs | 2 -- .../Compose/Components/BlueprintContainer.cs | 20 +++++++++---------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs index db27e20010..3f8d9f80d4 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs @@ -25,8 +25,6 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private SkinEditor editor { get; set; } = null!; - protected override bool AllowCyclicSelection => true; - public SkinBlueprintContainer(ISerialisableDrawableContainer targetContainer) { this.targetContainer = targetContainer; diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 5fdd2634c4..c8df999d37 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -46,15 +46,6 @@ namespace osu.Game.Screens.Edit.Compose.Components protected readonly BindableList SelectedItems = new BindableList(); - /// - /// Whether to allow cyclic selection on clicking multiple times. - /// - /// - /// Disabled by default as it does not work well with editors that support double-clicking or other advanced interactions. - /// Can probably be made to work with more thought. - /// - protected virtual bool AllowCyclicSelection => false; - protected BlueprintContainer() { RelativeSizeAxes = Axes.Both; @@ -167,6 +158,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (ClickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != ClickedBlueprint) return false; + doubleClickHandled = true; return true; } @@ -177,6 +169,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { endClickSelection(e); clickSelectionHandled = false; + doubleClickHandled = false; isDraggingBlueprint = false; wasDragStarted = false; }); @@ -376,6 +369,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// private bool clickSelectionHandled; + /// + /// Whether a blueprint was double-clicked since last mouse down. + /// + private bool doubleClickHandled; + /// /// Whether the selected blueprint(s) were already selected on mouse down. Generally used to perform selection cycling on mouse up in such a case. /// @@ -424,7 +422,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private bool endClickSelection(MouseButtonEvent e) { // If already handled a selection or drag, we don't want to perform a mouse up / click action. - if (clickSelectionHandled || isDraggingBlueprint) return true; + if (clickSelectionHandled || doubleClickHandled || isDraggingBlueprint) return true; if (e.Button != MouseButton.Left) return false; @@ -440,7 +438,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; } - if (!wasDragStarted && selectedBlueprintAlreadySelectedOnMouseDown && SelectedItems.Count == 1 && AllowCyclicSelection) + if (!wasDragStarted && selectedBlueprintAlreadySelectedOnMouseDown && SelectedItems.Count == 1) { // If a click occurred and was handled by the currently selected blueprint but didn't result in a drag, // cycle between other blueprints which are also under the cursor. From 94c5b8ed32a322d58b04b894eb65449b81bb9bf8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 17:22:25 +0900 Subject: [PATCH 0762/2100] Apply NRT to `TestSceneComposerSelection` --- .../Visual/Editing/TestSceneComposerSelection.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index d7fb13390f..51e75939fe 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using NUnit.Framework; @@ -82,7 +80,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestNudgeSelection() { - HitCircle[] addedObjects = null; + HitCircle[] addedObjects = null!; AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] { @@ -104,7 +102,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestRotateHotkeys() { - HitCircle[] addedObjects = null; + HitCircle[] addedObjects = null!; AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] { @@ -136,7 +134,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestGlobalFlipHotkeys() { - HitCircle addedObject = null; + HitCircle addedObject = null!; AddStep("add hitobjects", () => EditorBeatmap.Add(addedObject = new HitCircle { StartTime = 100 })); @@ -369,7 +367,7 @@ namespace osu.Game.Tests.Visual.Editing [TestCase(true)] public void TestMultiSelectFromDrag(bool alreadySelectedBeforeDrag) { - HitCircle[] addedObjects = null; + HitCircle[] addedObjects = null!; AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] { @@ -468,7 +466,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestQuickDeleteRemovesSliderControlPoint() { - Slider slider = null; + Slider slider = null!; PathControlPoint[] points = { From e283845b7144d1e16039841af85f3441a3be2a26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 17:45:07 +0900 Subject: [PATCH 0763/2100] Adjust legacy skin elements to better align with skinning expectations --- osu.Game/Skinning/LegacyAccuracyCounter.cs | 4 ++-- osu.Game/Skinning/LegacyScoreCounter.cs | 2 +- osu.Game/Skinning/LegacySkin.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index c99cdba91c..326257c25f 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -17,8 +17,8 @@ namespace osu.Game.Skinning Anchor = Anchor.TopRight; Origin = Anchor.TopRight; - Scale = new Vector2(0.6f); - Margin = new MarginPadding(10); + Scale = new Vector2(0.6f * 0.96f); + Margin = new MarginPadding { Vertical = 9, Horizontal = 17 }; } protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(LegacyFont.Score) diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index d8ee6b21de..d238369be1 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -21,7 +21,7 @@ namespace osu.Game.Skinning Origin = Anchor.TopRight; Scale = new Vector2(0.96f); - Margin = new MarginPadding(10); + Margin = new MarginPadding { Horizontal = 10 }; } protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(LegacyFont.Score) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 79f13686e8..00c18bef3d 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -368,7 +368,7 @@ namespace osu.Game.Skinning { songProgress.Anchor = Anchor.TopRight; songProgress.Origin = Anchor.CentreRight; - songProgress.X = -accuracy.ScreenSpaceDeltaToParentSpace(accuracy.ScreenSpaceDrawQuad.Size).X - 10; + songProgress.X = -accuracy.ScreenSpaceDeltaToParentSpace(accuracy.ScreenSpaceDrawQuad.Size).X - 20; songProgress.Y = container.ToLocalSpace(accuracy.ScreenSpaceDrawQuad.TopLeft).Y + (accuracy.ScreenSpaceDeltaToParentSpace(accuracy.ScreenSpaceDrawQuad.Size).Y / 2); } From 61ff3d08d45f2667215c46d1412cd9d3f02e3d5a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 17:48:19 +0900 Subject: [PATCH 0764/2100] Change depth of `LegacySongProgress` to allow "skinning" via health bar background --- osu.Game/Skinning/LegacySkin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 00c18bef3d..b72a757e6c 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -397,8 +397,8 @@ namespace osu.Game.Skinning new LegacyComboCounter(), new LegacyScoreCounter(), new LegacyAccuracyCounter(), - new LegacyHealthDisplay(), new LegacySongProgress(), + new LegacyHealthDisplay(), new BarHitErrorMeter(), new DefaultKeyCounterDisplay() } From eb149942e59b9995cfe31d0287289eb4842a0742 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 19:08:32 +0900 Subject: [PATCH 0765/2100] Add ability to toggle all free mods quickly at multiplayer song select --- .../TestSceneMultiplayerMatchSongSelect.cs | 8 ++ osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 + .../OnlinePlay/FooterButtonFreeMods.cs | 106 ++++++++++++++---- .../OnlinePlay/OnlinePlaySongSelect.cs | 9 +- 4 files changed, 104 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 947b7e5be6..8dc41cd707 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -67,6 +67,14 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded); } + [Test] + public void TestSelectFreeMods() + { + AddStep("set some freemods", () => songSelect.FreeMods.Value = new OsuRuleset().GetModsFor(ModType.Fun).ToArray()); + AddStep("set all freemods", () => songSelect.FreeMods.Value = new OsuRuleset().CreateAllMods().ToArray()); + AddStep("set no freemods", () => songSelect.FreeMods.Value = Array.Empty()); + } + [Test] public void TestBeatmapConfirmed() { diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index c09668850a..ba3f01a688 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -111,6 +111,10 @@ namespace osu.Game.Overlays.Mods private readonly Bindable>> globalAvailableMods = new Bindable>>(); + public IEnumerable AllAvailableAndValidMods => allAvailableMods + .Select(s => s.Mod) + .Where(m => isValidMod(m)); + private IEnumerable allAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value); private readonly BindableBool customisationVisible = new BindableBool(); diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 920aff13a8..56a69be741 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -1,15 +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; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Select; using osuTK; @@ -17,28 +23,60 @@ namespace osu.Game.Screens.OnlinePlay { public partial class FooterButtonFreeMods : FooterButton, IHasCurrentValue> { - public Bindable> Current + public Bindable> Current { get; set; } = new BindableWithCurrent>(); + + private OsuSpriteText count = null!; + + private Circle circle = null!; + + private readonly FreeModSelectOverlay freeModSelectOverlay; + + public FooterButtonFreeMods(FreeModSelectOverlay freeModSelectOverlay) { - get => modDisplay.Current; - set => modDisplay.Current = value; + this.freeModSelectOverlay = freeModSelectOverlay; } - private readonly ModDisplay modDisplay; - - public FooterButtonFreeMods() - { - ButtonContentContainer.Add(modDisplay = new ModDisplay - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(0.8f), - ExpansionMode = ExpansionMode.AlwaysContracted, - }); - } + [Resolved] + private OsuColour colours { get; set; } = null!; [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { + ButtonContentContainer.AddRange(new[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + circle = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colours.YellowDark, + RelativeSizeAxes = Axes.Both, + }, + count = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(5), + UseFullGlyphHeight = false, + } + } + }, + new IconButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.8f), + Icon = FontAwesome.Solid.Bars, + Action = () => freeModSelectOverlay.ToggleVisibility() + } + }); + SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); Text = @"freemods"; @@ -49,14 +87,42 @@ namespace osu.Game.Screens.OnlinePlay base.LoadComplete(); Current.BindValueChanged(_ => updateModDisplay(), true); + + // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. + Action = toggleAllFreeMods; + } + + /// + /// Immediately toggle all free mods on/off. + /// + private void toggleAllFreeMods() + { + var availableMods = freeModSelectOverlay.AllAvailableAndValidMods.ToArray(); + + Current.Value = Current.Value.Count == availableMods.Length + ? Array.Empty() + : availableMods; } private void updateModDisplay() { - if (Current.Value?.Count > 0) - modDisplay.FadeIn(); + int current = Current.Value.Count; + + if (current == freeModSelectOverlay.AllAvailableAndValidMods.Count()) + { + count.Text = "all"; + circle.FadeColour(colours.Yellow, 200, Easing.OutQuint); + } + else if (current > 0) + { + count.Text = $"{current} mods"; + circle.FadeColour(colours.YellowDark, 200, Easing.OutQuint); + } else - modDisplay.FadeOut(); + { + count.Text = "off"; + circle.FadeColour(colours.Gray4, 200, Easing.OutQuint); + } } } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index e0ae437d49..622ffddba6 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -175,9 +175,12 @@ namespace osu.Game.Screens.OnlinePlay protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateFooterButtons() { - var buttons = base.CreateFooterButtons().ToList(); - buttons.Insert(buttons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (new FooterButtonFreeMods { Current = FreeMods }, freeModSelectOverlay)); - return buttons; + var baseButtons = base.CreateFooterButtons().ToList(); + var freeModsButton = new FooterButtonFreeMods(freeModSelectOverlay) { Current = FreeMods }; + + baseButtons.Insert(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (freeModsButton, freeModSelectOverlay)); + + return baseButtons; } /// From 310067b4c358c5fa5210b5990e6fd76dbefe14cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 19:11:19 +0900 Subject: [PATCH 0766/2100] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 8febabb61b..4ea788a808 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From 18aace177a7fdcd9a6756229326b102f669c7d53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 19:36:51 +0900 Subject: [PATCH 0767/2100] Fix deadlock when logging out while at the create match screen Closes https://github.com/ppy/osu/issues/24275. --- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 9 +++++++++ .../Multiplayer/MultiplayerMatchSubScreen.cs | 14 ++++---------- osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs | 6 ++---- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 6b68024393..6e126a928a 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -23,6 +23,7 @@ using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Input.Bindings; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -76,6 +77,9 @@ namespace osu.Game.Screens.OnlinePlay.Match [Resolved] private RulesetStore rulesets { get; set; } + [Resolved] + private IAPIProvider api { get; set; } = null!; + [Resolved(canBeNull: true)] protected OnlinePlayScreen ParentScreen { get; private set; } @@ -284,6 +288,8 @@ namespace osu.Game.Screens.OnlinePlay.Match [Resolved(canBeNull: true)] private IDialogOverlay dialogOverlay { get; set; } + protected virtual bool IsConnected => api.State.Value == APIState.Online; + public override bool OnBackButton() { if (Room.RoomID.Value == null) @@ -356,6 +362,9 @@ namespace osu.Game.Screens.OnlinePlay.Match if (ExitConfirmed) return true; + if (!IsConnected) + return true; + if (dialogOverlay == null || Room.RoomID.Value != null || Room.Playlist.Count == 0) return true; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 01f04b44c9..f5746ca96c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -17,7 +17,6 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Cursor; using osu.Game.Online; -using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -50,9 +49,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } - [Resolved] - private IAPIProvider api { get; set; } - [Resolved(canBeNull: true)] private OsuGame game { get; set; } @@ -79,6 +75,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer handleRoomLost(); } + protected override bool IsConnected => base.IsConnected && client.IsConnected.Value; + protected override Drawable CreateMainContent() => new Container { RelativeSizeAxes = Axes.Both, @@ -254,13 +252,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public override bool OnExiting(ScreenExitEvent e) { - // the room may not be left immediately after a disconnection due to async flow, - // so checking the MultiplayerClient / IAPIAccess statuses is also required. - if (client.Room == null || !client.IsConnected.Value || api.State.Value != APIState.Online) - { - // room has not been created yet or we're offline; exit immediately. + // room has not been created yet or we're offline; exit immediately. + if (client.Room == null || !IsConnected) return base.OnExiting(e); - } if (!exitConfirmed && dialogOverlay != null) { diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs index c7b32131cf..b527bf98a2 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; @@ -15,8 +13,8 @@ namespace osu.Game.Screens.OnlinePlay public virtual string ShortTitle => Title; - [Resolved(CanBeNull = true)] - protected IRoomManager RoomManager { get; private set; } + [Resolved] + protected IRoomManager? RoomManager { get; private set; } protected OnlinePlaySubScreen() { From e47722565ae2c53ee38f156a027aa197e54e3c7c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 19:39:10 +0900 Subject: [PATCH 0768/2100] Clarify guard condition in `RoomSubScreen` --- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 6e126a928a..75b673cf1b 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -365,7 +365,9 @@ namespace osu.Game.Screens.OnlinePlay.Match if (!IsConnected) return true; - if (dialogOverlay == null || Room.RoomID.Value != null || Room.Playlist.Count == 0) + bool hasUnsavedChanges = Room.RoomID.Value == null && Room.Playlist.Count > 0; + + if (dialogOverlay == null || !hasUnsavedChanges) return true; // if the dialog is already displayed, block exiting until the user explicitly makes a decision. From 764029bde1eda3976982238bf9d1cba6ebe56fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Jul 2023 19:23:08 +0200 Subject: [PATCH 0769/2100] Fix nullability inspection --- .../OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index dd4f35cdd4..4478179726 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // To work around this, temporarily remove the room and trigger an immediate listing poll. if (e.Last is MultiplayerMatchSubScreen match) { - RoomManager.RemoveRoom(match.Room); + RoomManager?.RemoveRoom(match.Room); ListingPollingComponent.PollImmediately(); } } From 2c97ac74107c975f8f37e6a840caff06bef08fc8 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Wed, 19 Jul 2023 14:28:04 -0400 Subject: [PATCH 0770/2100] convert AliveObjects to list in hit policy instead of globally --- osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs | 8 +++++--- osu.Game/Rulesets/UI/HitObjectContainer.cs | 2 +- osu.Game/Rulesets/UI/IHitObjectContainer.cs | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs index 07942954e1..172e5a39d8 100644 --- a/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Linq; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -27,16 +28,17 @@ namespace osu.Game.Rulesets.Osu.UI public ClickAction CheckHittable(DrawableHitObject hitObject, double time) { - int index = HitObjectContainer.AliveObjects.IndexOf(hitObject); + var aliveObjects = HitObjectContainer.AliveObjects.ToList(); + int index = aliveObjects.IndexOf(hitObject); if (index > 0) { - var previousHitObject = (DrawableOsuHitObject)HitObjectContainer.AliveObjects[index - 1]; + var previousHitObject = (DrawableOsuHitObject)aliveObjects[index - 1]; if (previousHitObject.HitObject.StackHeight > 0 && !previousHitObject.AllJudged) return ClickAction.Ignore; } - foreach (DrawableHitObject testObject in HitObjectContainer.AliveObjects) + foreach (DrawableHitObject testObject in aliveObjects) { if (testObject.AllJudged) continue; diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 454a83bcda..099be486b3 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.UI { public IEnumerable Objects => InternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); - public IList AliveObjects => AliveEntries.Select(pair => pair.Drawable).OrderBy(h => h.HitObject.StartTime).ToList(); + public IEnumerable AliveObjects => AliveEntries.Select(pair => pair.Drawable).OrderBy(h => h.HitObject.StartTime); /// /// Invoked when a is judged. diff --git a/osu.Game/Rulesets/UI/IHitObjectContainer.cs b/osu.Game/Rulesets/UI/IHitObjectContainer.cs index bb4806206d..6dcb0944be 100644 --- a/osu.Game/Rulesets/UI/IHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/IHitObjectContainer.cs @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.UI /// /// If this uses pooled objects, this is equivalent to . /// - IList AliveObjects { get; } + IEnumerable AliveObjects { get; } } } From 6a8123029854e79f9124ea90f88c41e0245fda16 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Wed, 19 Jul 2023 14:44:28 -0400 Subject: [PATCH 0771/2100] rename ObjectOrderedHitPolicy to LegacyHitPolicy --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 2 +- .../UI/{ObjectOrderedHitPolicy.cs => LegacyHitPolicy.cs} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename osu.Game.Rulesets.Osu/UI/{ObjectOrderedHitPolicy.cs => LegacyHitPolicy.cs} (97%) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 250d97c537..229f80c2bd 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Mods var osuRuleset = (DrawableOsuRuleset)drawableRuleset; if (ClassicNoteLock.Value) - osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy(); + osuRuleset.Playfield.HitPolicy = new LegacyHitPolicy(); usingHiddenFading = drawableRuleset.Mods.OfType().SingleOrDefault()?.OnlyFadeApproachCircles.Value == false; } diff --git a/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/LegacyHitPolicy.cs similarity index 97% rename from osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs rename to osu.Game.Rulesets.Osu/UI/LegacyHitPolicy.cs index 172e5a39d8..c35d4a1b56 100644 --- a/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/LegacyHitPolicy.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.UI /// Hits will be blocked until the previous s have been judged. /// /// - public class ObjectOrderedHitPolicy : IHitPolicy + public class LegacyHitPolicy : IHitPolicy { public IHitObjectContainer HitObjectContainer { get; set; } From fa29c25097a178ad04a08c5c8ab26a3c918e847e Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Thu, 20 Jul 2023 00:32:35 +0300 Subject: [PATCH 0772/2100] Change check to use binary search --- osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs | 57 ++++++++++++++++---- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs b/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs index 54dfb557fe..44d2c18dad 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs @@ -34,23 +34,62 @@ namespace osu.Game.Rulesets.Edit.Checks { if (breakPeriod.Duration < BreakPeriod.MIN_BREAK_DURATION) yield return new IssueTemplateTooShort(this).Create(breakPeriod.StartTime); - } - foreach (var hitObject in context.Beatmap.HitObjects) - { - foreach (var breakPeriod in context.Beatmap.Breaks) + var previousObject = getPreviousObject(breakPeriod.StartTime, context.Beatmap.HitObjects); + var nextObject = getNextObject(breakPeriod.EndTime, context.Beatmap.HitObjects); + + if (previousObject != null) { - double gapBeforeBreak = breakPeriod.StartTime - hitObject.GetEndTime(); - double gapAfterBreak = hitObject.StartTime - breakPeriod.EndTime; - - if (gapBeforeBreak < minimum_gap_before_break - leniency_threshold && gapBeforeBreak > 0) + double gapBeforeBreak = breakPeriod.StartTime - previousObject.GetEndTime(); + if (gapBeforeBreak < minimum_gap_before_break - leniency_threshold) yield return new IssueTemplateEarlyStart(this).Create(breakPeriod.StartTime, minimum_gap_before_break - gapBeforeBreak); - else if (gapAfterBreak < min_end_threshold - leniency_threshold && gapAfterBreak > 0) + } + + if (nextObject != null) + { + double gapAfterBreak = nextObject.StartTime - breakPeriod.EndTime; + if (gapAfterBreak < min_end_threshold - leniency_threshold) yield return new IssueTemplateLateEnd(this).Create(breakPeriod.StartTime, min_end_threshold - gapAfterBreak); } } } + private HitObject? getPreviousObject(double time, IReadOnlyList hitObjects) + { + int left = 0; + int right = hitObjects.Count - 1; + + while (left <= right) + { + int mid = left + (right - left) / 2; + + if (hitObjects[mid].GetEndTime() < time) + left = mid + 1; + else + right = mid - 1; + } + + return right >= 0 ? hitObjects[right] : null; + } + + private HitObject? getNextObject(double time, IReadOnlyList hitObjects) + { + int left = 0; + int right = hitObjects.Count - 1; + + while (left <= right) + { + int mid = left + (right - left) / 2; + + if (hitObjects[mid].StartTime <= time) + left = mid + 1; + else + right = mid - 1; + } + + return left < hitObjects.Count ? hitObjects[left] : null; + } + public class IssueTemplateEarlyStart : IssueTemplate { public IssueTemplateEarlyStart(ICheck check) From ce78bb549f2e5d9c09bf91402f55a7d26555f191 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Thu, 20 Jul 2023 00:32:54 +0300 Subject: [PATCH 0773/2100] Add test for multiple too early objects in break --- .../Editing/Checks/CheckBreaksTest.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs b/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs index 39e414827a..aaa536f9b9 100644 --- a/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs @@ -88,6 +88,30 @@ namespace osu.Game.Tests.Editing.Checks Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateLateEnd); } + [Test] + public void TestBreakMultipleObjectsEarly() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 1_297 }, + new HitCircle { StartTime = 1_298 } + }, + Breaks = new List + { + new BreakPeriod(200, 850) + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateLateEnd); + } + [Test] public void TestBreaksCorrect() { From 18c5fc689f4dc799ab433eb041ca9c09482668ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Jul 2023 12:58:13 +0900 Subject: [PATCH 0774/2100] Don't expose such specific information from `ModSelectOverlay` --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 14 +++++--------- .../Screens/OnlinePlay/FooterButtonFreeMods.cs | 8 ++++++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index ba3f01a688..7ec108e3ec 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -111,11 +111,7 @@ namespace osu.Game.Overlays.Mods private readonly Bindable>> globalAvailableMods = new Bindable>>(); - public IEnumerable AllAvailableAndValidMods => allAvailableMods - .Select(s => s.Mod) - .Where(m => isValidMod(m)); - - private IEnumerable allAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value); + public IEnumerable AllAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value); private readonly BindableBool customisationVisible = new BindableBool(); @@ -386,7 +382,7 @@ namespace osu.Game.Overlays.Mods private void filterMods() { - foreach (var modState in allAvailableMods) + foreach (var modState in AllAvailableMods) modState.ValidForSelection.Value = modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod); } @@ -411,7 +407,7 @@ namespace osu.Game.Overlays.Mods bool anyCustomisableModActive = false; bool anyModPendingConfiguration = false; - foreach (var modState in allAvailableMods) + foreach (var modState in AllAvailableMods) { anyCustomisableModActive |= modState.Active.Value && modState.Mod.GetSettingsSourceProperties().Any(); anyModPendingConfiguration |= modState.PendingConfiguration; @@ -468,7 +464,7 @@ namespace osu.Game.Overlays.Mods var newSelection = new List(); - foreach (var modState in allAvailableMods) + foreach (var modState in AllAvailableMods) { var matchingSelectedMod = SelectedMods.Value.SingleOrDefault(selected => selected.GetType() == modState.Mod.GetType()); @@ -495,7 +491,7 @@ namespace osu.Game.Overlays.Mods if (externalSelectionUpdateInProgress) return; - var candidateSelection = allAvailableMods.Where(modState => modState.Active.Value) + var candidateSelection = AllAvailableMods.Where(modState => modState.Active.Value) .Select(modState => modState.Mod) .ToArray(); diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 56a69be741..294c80677d 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -97,7 +97,7 @@ namespace osu.Game.Screens.OnlinePlay /// private void toggleAllFreeMods() { - var availableMods = freeModSelectOverlay.AllAvailableAndValidMods.ToArray(); + var availableMods = allAvailableAndValidMods.ToArray(); Current.Value = Current.Value.Count == availableMods.Length ? Array.Empty() @@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay { int current = Current.Value.Count; - if (current == freeModSelectOverlay.AllAvailableAndValidMods.Count()) + if (current == allAvailableAndValidMods.Count()) { count.Text = "all"; circle.FadeColour(colours.Yellow, 200, Easing.OutQuint); @@ -124,5 +124,9 @@ namespace osu.Game.Screens.OnlinePlay circle.FadeColour(colours.Gray4, 200, Easing.OutQuint); } } + + private IEnumerable allAvailableAndValidMods => freeModSelectOverlay.AllAvailableMods + .Where(state => state.ValidForSelection.Value) + .Select(state => state.Mod); } } From c93d6a4008639b68a9f6508d6b5e46f9e800df9d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Jul 2023 13:04:21 +0900 Subject: [PATCH 0775/2100] Invert text colour when freemods is enabled for better contrast --- osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 294c80677d..3825cf18b9 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -111,16 +111,19 @@ namespace osu.Game.Screens.OnlinePlay if (current == allAvailableAndValidMods.Count()) { count.Text = "all"; + count.FadeColour(colours.Gray2, 200, Easing.OutQuint); circle.FadeColour(colours.Yellow, 200, Easing.OutQuint); } else if (current > 0) { count.Text = $"{current} mods"; + count.FadeColour(colours.Gray2, 200, Easing.OutQuint); circle.FadeColour(colours.YellowDark, 200, Easing.OutQuint); } else { count.Text = "off"; + count.FadeColour(colours.GrayF, 200, Easing.OutQuint); circle.FadeColour(colours.Gray4, 200, Easing.OutQuint); } } From d9d055361aa956e5233d38c689aa9077033d104a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Jul 2023 17:49:23 +0900 Subject: [PATCH 0776/2100] More realm analytic disables --- .gitignore | 1 - osu.Game/FodyWeavers.xml | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 osu.Game/FodyWeavers.xml diff --git a/.gitignore b/.gitignore index 0c7a18b437..525b3418cd 100644 --- a/.gitignore +++ b/.gitignore @@ -339,6 +339,5 @@ inspectcode # Fody (pulled in by Realm) - schema file FodyWeavers.xsd -**/FodyWeavers.xml .idea/.idea.osu.Desktop/.idea/misc.xml \ No newline at end of file diff --git a/osu.Game/FodyWeavers.xml b/osu.Game/FodyWeavers.xml new file mode 100644 index 0000000000..7ff486f40c --- /dev/null +++ b/osu.Game/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + From f791f21dcb3bc22c439838f2f7bee5d84310b757 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Jul 2023 20:05:35 +0900 Subject: [PATCH 0777/2100] Update framework --- 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 059c5f26e7..ed97f609cc 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + - true $(NoWarn);MT7091 + + + true + + + + false + -all + ios-arm64 From 97075b0726f82cec03e4bb5a533e9e6d11bc8c1b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 23 Jul 2023 20:31:26 +0300 Subject: [PATCH 0847/2100] Fix iOS visual tests having unusual bundle identifiers --- 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 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist index 5ace6c07f5..f87043e1d1 100644 --- a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist @@ -5,7 +5,7 @@ CFBundleName osu.Game.Rulesets.Catch.Tests.iOS CFBundleIdentifier - ppy.osu-Game-Rulesets-Catch-Tests-iOS + sh.ppy.catch-ruleset-tests CFBundleShortVersionString 1.0 CFBundleVersion @@ -42,4 +42,4 @@ CADisableMinimumFrameDurationOnPhone - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist index ff5dde856e..740036309f 100644 --- a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist @@ -5,7 +5,7 @@ CFBundleName osu.Game.Rulesets.Mania.Tests.iOS CFBundleIdentifier - ppy.osu-Game-Rulesets-Mania-Tests-iOS + sh.ppy.mania-ruleset-tests CFBundleShortVersionString 1.0 CFBundleVersion @@ -42,4 +42,4 @@ CADisableMinimumFrameDurationOnPhone - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist index 1e33f2ff16..7f489874e7 100644 --- a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist @@ -5,7 +5,7 @@ CFBundleName osu.Game.Rulesets.Osu.Tests.iOS CFBundleIdentifier - ppy.osu-Game-Rulesets-Osu-Tests-iOS + sh.ppy.osu-ruleset-tests CFBundleShortVersionString 1.0 CFBundleVersion @@ -42,4 +42,4 @@ CADisableMinimumFrameDurationOnPhone - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist index 76cb3c0db0..162ee75c22 100644 --- a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist @@ -5,7 +5,7 @@ CFBundleName osu.Game.Rulesets.Taiko.Tests.iOS CFBundleIdentifier - ppy.osu-Game-Rulesets-Taiko-Tests-iOS + sh.ppy.taiko-ruleset-tests CFBundleShortVersionString 1.0 CFBundleVersion @@ -42,4 +42,4 @@ CADisableMinimumFrameDurationOnPhone - + \ No newline at end of file diff --git a/osu.Game.Tests.iOS/Info.plist b/osu.Game.Tests.iOS/Info.plist index ac661f6263..d2d0583e46 100644 --- a/osu.Game.Tests.iOS/Info.plist +++ b/osu.Game.Tests.iOS/Info.plist @@ -5,7 +5,7 @@ CFBundleName osu.Game.Tests.iOS CFBundleIdentifier - ppy.osu-Game-Tests-iOS + sh.ppy.osu-tests CFBundleShortVersionString 1.0 CFBundleVersion @@ -42,4 +42,4 @@ CADisableMinimumFrameDurationOnPhone - + \ No newline at end of file From ba8ebefb50dca3a31462ecf4b282036ada782991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 18:09:07 +0200 Subject: [PATCH 0848/2100] Add basic structure for new rotation handler --- .../Compose/Components/SelectionHandler.cs | 9 ++++++ .../Components/SelectionRotationHandler.cs | 31 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 9b44b15fe4..80df796fd7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -55,6 +55,8 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved(CanBeNull = true)] protected IEditorChangeHandler ChangeHandler { get; private set; } + protected SelectionRotationHandler RotationHandler { get; private set; } + protected SelectionHandler() { selectedBlueprints = new List>(); @@ -66,6 +68,8 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load() { + RotationHandler = CreateRotationHandler(); + InternalChild = SelectionBox = CreateSelectionBox(); SelectedItems.CollectionChanged += (_, _) => @@ -132,6 +136,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether any items could be rotated. public virtual bool HandleRotation(float angle) => false; + /// + /// Creates the handler to use for rotation operations. + /// + public virtual SelectionRotationHandler CreateRotationHandler() => new SelectionRotationHandler(); + /// /// Handles the selected items being scaled. /// diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs new file mode 100644 index 0000000000..595edbb4fc --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osuTK; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + /// + /// Base handler for editor rotation operations. + /// + public class SelectionRotationHandler + { + /// + /// Whether the rotation can currently be performed. + /// + public Bindable CanRotate { get; private set; } = new BindableBool(); + + public virtual void Begin() + { + } + + public virtual void Update(float rotation, Vector2 origin) + { + } + + public virtual void Commit() + { + } + } +} From ba904fd77bbb530b505817b32dbb31ac0e5baa1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 19:18:38 +0200 Subject: [PATCH 0849/2100] Migrate osu! rotation handling to `SelectionRotationHandler` --- .../Edit/OsuSelectionHandler.cs | 30 +----- .../Edit/OsuSelectionRotationHandler.cs | 98 +++++++++++++++++++ 2 files changed, 101 insertions(+), 27 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 468d8ae9f5..1d46b8ff8a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -28,11 +28,6 @@ namespace osu.Game.Rulesets.Osu.Edit [Resolved(CanBeNull = true)] private IDistanceSnapProvider? snapProvider { get; set; } - /// - /// During a transform, the initial origin is stored so it can be used throughout the operation. - /// - private Vector2? referenceOrigin; - /// /// During a transform, the initial path types of a single selected slider are stored so they /// can be maintained throughout the operation. @@ -54,7 +49,6 @@ namespace osu.Game.Rulesets.Osu.Edit protected override void OnOperationEnded() { base.OnOperationEnded(); - referenceOrigin = null; referencePathTypes = null; } @@ -170,28 +164,10 @@ namespace osu.Game.Rulesets.Osu.Edit if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y; } - public override bool HandleRotation(float delta) + public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler(ChangeHandler) { - var hitObjects = selectedMovableObjects; - - Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects); - - referenceOrigin ??= quad.Centre; - - foreach (var h in hitObjects) - { - h.Position = GeometryUtils.RotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta); - - if (h is IHasPath path) - { - foreach (PathControlPoint cp in path.Path.ControlPoints) - cp.Position = GeometryUtils.RotatePointAroundOrigin(cp.Position, Vector2.Zero, delta); - } - } - - // this isn't always the case but let's be lenient for now. - return true; - } + SelectedItems = { BindTarget = SelectedItems } + }; private void scaleSlider(Slider slider, Vector2 scale) { diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs new file mode 100644 index 0000000000..0eb7637786 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs @@ -0,0 +1,98 @@ +// 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.Diagnostics; +using System.Linq; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public class OsuSelectionRotationHandler : SelectionRotationHandler + { + private readonly IEditorChangeHandler? changeHandler; + + public BindableList SelectedItems { get; } = new BindableList(); + + public OsuSelectionRotationHandler(IEditorChangeHandler? changeHandler) + { + this.changeHandler = changeHandler; + + SelectedItems.CollectionChanged += (_, __) => updateState(); + } + + private void updateState() + { + var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects); + CanRotate.Value = quad.Width > 0 || quad.Height > 0; + } + + private OsuHitObject[]? objectsInRotation; + + private Vector2? defaultOrigin; + private Dictionary? originalPositions; + private Dictionary? originalPathControlPointPositions; + + public override void Begin() + { + if (objectsInRotation != null) + throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!"); + + changeHandler?.BeginChange(); + + objectsInRotation = selectedMovableObjects.ToArray(); + defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation).Centre; + originalPositions = objectsInRotation.ToDictionary(obj => obj, obj => obj.Position); + originalPathControlPointPositions = objectsInRotation.OfType().ToDictionary( + obj => obj, + obj => obj.Path.ControlPoints.Select(point => point.Position).ToArray()); + } + + public override void Update(float rotation, Vector2? origin = null) + { + if (objectsInRotation == null) + throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!"); + + Debug.Assert(originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null); + + Vector2 actualOrigin = origin ?? defaultOrigin.Value; + + foreach (var ho in objectsInRotation) + { + ho.Position = GeometryUtils.RotatePointAroundOrigin(originalPositions[ho], actualOrigin, rotation); + + if (ho is IHasPath withPath) + { + var originalPath = originalPathControlPointPositions[withPath]; + + for (int i = 0; i < withPath.Path.ControlPoints.Count; ++i) + withPath.Path.ControlPoints[i].Position = GeometryUtils.RotatePointAroundOrigin(originalPath[i], Vector2.Zero, rotation); + } + } + } + + public override void Commit() + { + if (objectsInRotation == null) + throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); + + changeHandler?.EndChange(); + + objectsInRotation = null; + originalPositions = null; + originalPathControlPointPositions = null; + defaultOrigin = null; + } + + private IEnumerable selectedMovableObjects => SelectedItems.Cast() + .Where(h => h is not Spinner); + } +} From f8047d6ab6d96bf9c7b87fcf50b93e2b084da2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 19:48:39 +0200 Subject: [PATCH 0850/2100] Migrate skin element rotation handling to `SelectionRotationHandler` --- .../SkinEditor/SkinSelectionHandler.cs | 29 +----- .../SkinSelectionRotationHandler.cs | 94 +++++++++++++++++++ 2 files changed, 98 insertions(+), 25 deletions(-) create mode 100644 osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index 4a1ddd9d69..bee973bea0 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -26,31 +26,11 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private SkinEditor skinEditor { get; set; } = null!; - public override bool HandleRotation(float angle) + public override SelectionRotationHandler CreateRotationHandler() => new SkinSelectionRotationHandler(ChangeHandler) { - if (SelectedBlueprints.Count == 1) - { - // for single items, rotate around the origin rather than the selection centre. - ((Drawable)SelectedBlueprints.First().Item).Rotation += angle; - } - else - { - var selectionQuad = getSelectionQuad(); - - foreach (var b in SelectedBlueprints) - { - var drawableItem = (Drawable)b.Item; - - var rotatedPosition = GeometryUtils.RotatePointAroundOrigin(b.ScreenSpaceSelectionPoint, selectionQuad.Centre, angle); - updateDrawablePosition(drawableItem, rotatedPosition); - - drawableItem.Rotation += angle; - } - } - - // this isn't always the case but let's be lenient for now. - return true; - } + SelectedItems = { BindTarget = SelectedItems }, + UpdatePosition = updateDrawablePosition + }; public override bool HandleScale(Vector2 scale, Anchor anchor) { @@ -172,7 +152,6 @@ namespace osu.Game.Overlays.SkinEditor { base.OnSelectionChanged(); - SelectionBox.CanRotate = true; SelectionBox.CanScaleX = true; SelectionBox.CanScaleY = true; SelectionBox.CanFlipX = true; diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs new file mode 100644 index 0000000000..e60e2b1e12 --- /dev/null +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs @@ -0,0 +1,94 @@ +// 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.Diagnostics; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Skinning; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Overlays.SkinEditor +{ + public class SkinSelectionRotationHandler : SelectionRotationHandler + { + private readonly IEditorChangeHandler? changeHandler; + + public BindableList SelectedItems { get; } = new BindableList(); + public Action UpdatePosition { get; init; } = null!; + + public SkinSelectionRotationHandler(IEditorChangeHandler? changeHandler) + { + this.changeHandler = changeHandler; + + SelectedItems.CollectionChanged += (_, __) => updateState(); + } + + private void updateState() + { + CanRotate.Value = SelectedItems.Count > 0; + } + + private Drawable[]? objectsInRotation; + + private Vector2? defaultOrigin; + private Dictionary? originalRotations; + private Dictionary? originalPositions; + + public override void Begin() + { + if (objectsInRotation != null) + throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!"); + + changeHandler?.BeginChange(); + + objectsInRotation = SelectedItems.Cast().ToArray(); + originalRotations = objectsInRotation.ToDictionary(d => d, d => d.Rotation); + originalPositions = objectsInRotation.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition)); + defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())).Centre; + } + + public override void Update(float rotation, Vector2? origin = null) + { + if (objectsInRotation == null) + throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!"); + + Debug.Assert(originalRotations != null && originalPositions != null && defaultOrigin != null); + + if (objectsInRotation.Length == 1 && origin == null) + { + // for single items, rotate around the origin rather than the selection centre by default. + objectsInRotation[0].Rotation = originalRotations.Single().Value + rotation; + return; + } + + var actualOrigin = origin ?? defaultOrigin.Value; + + foreach (var drawableItem in objectsInRotation) + { + var rotatedPosition = GeometryUtils.RotatePointAroundOrigin(originalPositions[drawableItem], actualOrigin, rotation); + UpdatePosition(drawableItem, rotatedPosition); + + drawableItem.Rotation = originalRotations[drawableItem] + rotation; + } + } + + public override void Commit() + { + if (objectsInRotation == null) + throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); + + changeHandler?.EndChange(); + + objectsInRotation = null; + originalPositions = null; + originalRotations = null; + defaultOrigin = null; + } + } +} From 21df0e2d60824ea1e34cd88f60741484d251d049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 19:57:31 +0200 Subject: [PATCH 0851/2100] Migrate test to `SelectionRotationHandler` --- .../Editing/TestSceneComposeSelectBox.cs | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 7a0b3d0c1a..147488812e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -3,7 +3,9 @@ #nullable disable +using System; using System.Linq; +using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -34,13 +36,12 @@ namespace osu.Game.Tests.Visual.Editing { RelativeSizeAxes = Axes.Both, - CanRotate = true, CanScaleX = true, CanScaleY = true, CanFlipX = true, CanFlipY = true, - OnRotation = handleRotation, + RotationHandler = new TestSelectionRotationHandler(() => selectionArea), OnScale = handleScale } } @@ -71,11 +72,48 @@ namespace osu.Game.Tests.Visual.Editing return true; } - private bool handleRotation(float angle) + private class TestSelectionRotationHandler : SelectionRotationHandler { - // kinda silly and wrong, but just showing that the drag handles work. - selectionArea.Rotation += angle; - return true; + private readonly Func getTargetContainer; + + public TestSelectionRotationHandler(Func getTargetContainer) + { + this.getTargetContainer = getTargetContainer; + + CanRotate.Value = true; + } + + [CanBeNull] + private Container targetContainer; + + private float? initialRotation; + + public override void Begin() + { + if (targetContainer != null) + throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!"); + + targetContainer = getTargetContainer(); + initialRotation = targetContainer!.Rotation; + } + + public override void Update(float rotation, Vector2? origin = null) + { + if (targetContainer == null) + throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!"); + + // kinda silly and wrong, but just showing that the drag handles work. + targetContainer.Rotation = initialRotation!.Value + rotation; + } + + public override void Commit() + { + if (targetContainer == null) + throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); + + targetContainer = null; + initialRotation = null; + } } [Test] From aec3ca250cc3301415d0ba38bc0058b2a2463205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 20:01:30 +0200 Subject: [PATCH 0852/2100] Migrate `SelectionHandler` to use `SelectionRotationHandler` --- .../Edit/OsuSelectionHandler.cs | 1 - .../Edit/Compose/Components/SelectionBox.cs | 41 ++++++++----------- .../Components/SelectionBoxRotationHandle.cs | 20 +++++---- .../Compose/Components/SelectionHandler.cs | 2 +- .../Components/SelectionRotationHandler.cs | 9 +++- 5 files changed, 37 insertions(+), 36 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 1d46b8ff8a..1dfbf4179b 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Osu.Edit Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad(); - SelectionBox.CanRotate = quad.Width > 0 || quad.Height > 0; SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0; SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0; SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 5d9fac739c..53442071b5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -22,7 +23,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private const float button_padding = 5; - public Func? OnRotation; + public SelectionRotationHandler? RotationHandler { get; init; } public Func? OnScale; public Func? OnFlip; public Func? OnReverse; @@ -51,22 +52,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - private bool canRotate; - - /// - /// Whether rotation support should be enabled. - /// - public bool CanRotate - { - get => canRotate; - set - { - if (canRotate == value) return; - - canRotate = value; - recreate(); - } - } + private IBindable canRotate = new BindableBool(); private bool canScaleX; @@ -161,7 +147,14 @@ namespace osu.Game.Screens.Edit.Compose.Components private OsuColour colours { get; set; } = null!; [BackgroundDependencyLoader] - private void load() => recreate(); + private void load() + { + if (RotationHandler != null) + canRotate.BindTo(RotationHandler.CanRotate); + + canRotate.BindValueChanged(_ => recreate()); + recreate(); + } protected override bool OnKeyDown(KeyDownEvent e) { @@ -174,10 +167,10 @@ namespace osu.Game.Screens.Edit.Compose.Components return CanReverse && reverseButton?.TriggerClick() == true; case Key.Comma: - return CanRotate && rotateCounterClockwiseButton?.TriggerClick() == true; + return canRotate.Value && rotateCounterClockwiseButton?.TriggerClick() == true; case Key.Period: - return CanRotate && rotateClockwiseButton?.TriggerClick() == true; + return canRotate.Value && rotateClockwiseButton?.TriggerClick() == true; } return base.OnKeyDown(e); @@ -254,14 +247,14 @@ namespace osu.Game.Screens.Edit.Compose.Components if (CanScaleY) addYScaleComponents(); if (CanFlipX) addXFlipComponents(); if (CanFlipY) addYFlipComponents(); - if (CanRotate) addRotationComponents(); + if (canRotate.Value) addRotationComponents(); if (CanReverse) reverseButton = addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke()); } private void addRotationComponents() { - rotateCounterClockwiseButton = addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise (Ctrl-<)", () => OnRotation?.Invoke(-90)); - rotateClockwiseButton = addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise (Ctrl->)", () => OnRotation?.Invoke(90)); + rotateCounterClockwiseButton = addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise (Ctrl-<)", () => RotationHandler?.Rotate(-90)); + rotateClockwiseButton = addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise (Ctrl->)", () => RotationHandler?.Rotate(90)); addRotateHandle(Anchor.TopLeft); addRotateHandle(Anchor.TopRight); @@ -331,7 +324,7 @@ namespace osu.Game.Screens.Edit.Compose.Components var handle = new SelectionBoxRotationHandle { Anchor = anchor, - HandleRotate = angle => OnRotation?.Invoke(angle) + RotationHandler = RotationHandler }; handle.OperationStarted += operationStarted; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index c2a3f12efd..4107a09692 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; @@ -21,7 +22,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { public partial class SelectionBoxRotationHandle : SelectionBoxDragHandle, IHasTooltip { - public Action HandleRotate { get; set; } + [CanBeNull] + public SelectionRotationHandler RotationHandler { get; init; } public LocalisableString TooltipText { get; private set; } @@ -63,10 +65,10 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnDragStart(DragStartEvent e) { - bool handle = base.OnDragStart(e); - if (handle) - cumulativeRotation.Value = 0; - return handle; + if (RotationHandler == null) return false; + + RotationHandler.Begin(); + return true; } protected override void OnDrag(DragEvent e) @@ -99,7 +101,9 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void OnDragEnd(DragEndEvent e) { - base.OnDragEnd(e); + RotationHandler?.Commit(); + UpdateHoverState(); + cumulativeRotation.Value = null; rawCumulativeRotation = 0; TooltipText = default; @@ -116,14 +120,12 @@ namespace osu.Game.Screens.Edit.Compose.Components private void applyRotation(bool shouldSnap) { - float oldRotation = cumulativeRotation.Value ?? 0; - float newRotation = shouldSnap ? snap(rawCumulativeRotation, snap_step) : MathF.Round(rawCumulativeRotation); newRotation = (newRotation - 180) % 360 + 180; cumulativeRotation.Value = newRotation; - HandleRotate?.Invoke(newRotation - oldRotation); + RotationHandler?.Update(newRotation); TooltipText = shouldSnap ? EditorStrings.RotationSnapped(newRotation) : EditorStrings.RotationUnsnapped(newRotation); } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 80df796fd7..31ad8fa3d7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -84,7 +84,7 @@ namespace osu.Game.Screens.Edit.Compose.Components OperationStarted = OnOperationBegan, OperationEnded = OnOperationEnded, - OnRotation = HandleRotation, + RotationHandler = RotationHandler, OnScale = HandleScale, OnFlip = HandleFlip, OnReverse = HandleReverse, diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index 595edbb4fc..d5dd1d38d4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -16,11 +16,18 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public Bindable CanRotate { get; private set; } = new BindableBool(); + public void Rotate(float rotation, Vector2? origin = null) + { + Begin(); + Update(rotation, origin); + Commit(); + } + public virtual void Begin() { } - public virtual void Update(float rotation, Vector2 origin) + public virtual void Update(float rotation, Vector2? origin = null) { } From a201152b042e0dfb13a4abaca85def6b5fc23577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 20:09:31 +0200 Subject: [PATCH 0853/2100] Add xmldoc to `SelectionRotationHandler` --- .../Components/SelectionRotationHandler.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index d5dd1d38d4..6524f7fa35 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -16,6 +16,18 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public Bindable CanRotate { get; private set; } = new BindableBool(); + /// + /// Performs a single, instant, atomic rotation operation. + /// + /// + /// This method is intended to be used in atomic contexts (such as when pressing a single button). + /// For continuous operations, see the -- flow. + /// + /// Rotation to apply in degrees. + /// + /// The origin point to rotate around. + /// If the default value is supplied, a sane implementation-defined default will be used. + /// public void Rotate(float rotation, Vector2? origin = null) { Begin(); @@ -23,14 +35,48 @@ namespace osu.Game.Screens.Edit.Compose.Components Commit(); } + /// + /// Begins a continuous rotation operation. + /// + /// + /// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider). + /// For instantaneous, atomic operations, use the convenience method. + /// public virtual void Begin() { } + /// + /// Updates a continuous rotation operation. + /// Must be preceded by a call. + /// + /// + /// + /// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider). + /// As such, the values of and supplied should be relative to the state of the objects being rotated + /// when was called, rather than instantaneous deltas. + /// + /// + /// For instantaneous, atomic operations, use the convenience method. + /// + /// + /// Rotation to apply in degrees. + /// + /// The origin point to rotate around. + /// If the default value is supplied, a sane implementation-defined default will be used. + /// public virtual void Update(float rotation, Vector2? origin = null) { } + /// + /// Ends a continuous rotation operation. + /// Must be preceded by a call. + /// + /// + /// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider). + /// For instantaneous, atomic operations, use the convenience method. + /// public virtual void Commit() { } From bf89fbcd817aab251797b40d8a4942103c360109 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jul 2023 16:38:37 +0900 Subject: [PATCH 0854/2100] Update framework --- 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 ed97f609cc..7f15d9fafd 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml index 4a1545a423..f5a49210ea 100644 --- a/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml +++ b/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml index 45d27dda70..ed4725dd94 100644 --- a/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml +++ b/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml index 452b9683ec..cc88d3080a 100644 --- a/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml +++ b/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/osu.Game.Tests.Android/AndroidManifest.xml b/osu.Game.Tests.Android/AndroidManifest.xml index f25b2e5328..6f91fb928c 100644 --- a/osu.Game.Tests.Android/AndroidManifest.xml +++ b/osu.Game.Tests.Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file From 912f31dabc7160fbd32ccb0a47fd733e427274e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Aug 2023 23:37:11 +0200 Subject: [PATCH 1123/2100] Declare media permissions in game project for editor usage --- osu.Android/AndroidManifest.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Android/AndroidManifest.xml b/osu.Android/AndroidManifest.xml index fb54c8e151..af102a1e4e 100644 --- a/osu.Android/AndroidManifest.xml +++ b/osu.Android/AndroidManifest.xml @@ -2,4 +2,7 @@ + + + \ No newline at end of file From a942b6ff745193ff9b3b0f8c624d5905a4495225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Aug 2023 07:27:02 +0200 Subject: [PATCH 1124/2100] Replace inline comment with actual explanation of what's happening --- osu.Game/Database/LegacyBeatmapExporter.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index c00977d072..ece705f685 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -72,7 +72,16 @@ namespace osu.Game.Database if (hitObject is not IHasPath hasPath) continue; - // Make sure the last control point is inherit type + // stable's hit object parsing expects the entire slider to use only one type of curve, + // and happens to use the last non-empty curve type read for the entire slider. + // this clear of the last control point type handles an edge case + // wherein the last control point of an otherwise-single-segment slider path has a different type than previous, + // which would lead to sliders being mangled when exported back to stable. + // normally, that would be handled by the `BezierConverter.ConvertToModernBezier()` call below, + // which outputs a slider path containing only Bezier control points, + // but a non-inherited last control point is (rightly) not considered to be starting a new segment, + // therefore it would fail to clear the `CountSegments() <= 1` check. + // by clearing explicitly we both fix the issue and avoid unnecessary conversions to Bezier. if (hasPath.Path.ControlPoints.Count > 1) hasPath.Path.ControlPoints[^1].Type = null; From dd1ac461db2c779c5f9a24bfedbdb3437f503c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Aug 2023 08:23:58 +0200 Subject: [PATCH 1125/2100] Reformat xmldoc --- osu.Game/Rulesets/Objects/SliderPath.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 1c02b18a0f..34113285a4 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -201,13 +201,20 @@ namespace osu.Game.Rulesets.Objects /// Returns the progress values at which (control point) segments of the path end. /// Ranges from 0 (beginning of the path) to 1 (end of the path) to infinity (beyond the end of the path). /// - /// In case is less than , + /// + /// truncates the progression values to [0,1], + /// so you can't use this method in conjunction with that one to retrieve the positions of segment ends beyond the end of the path. + /// + /// + /// + /// In case is less than , /// the last segment ends after the end of the path, hence it returns a value greater than 1. - /// + /// + /// /// In case is greater than , - /// the last segment ends before the end of the path, hence it returns a value less than 1. - /// truncates the progression values to [0,1], - /// so you can't use this method to retrieve the positions of segment ends beyond the end of the path. + /// the last segment ends before the end of the path, hence it returns a value less than 1. + /// + /// public IEnumerable GetSegmentEnds() { ensureValid(); From 479c463751e0159049bb63fe27937072a9b836c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Aug 2023 08:27:08 +0200 Subject: [PATCH 1126/2100] Explain why segment end positions are not recovered in test --- osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs index 16e4ae13d9..635d9f9604 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs @@ -207,6 +207,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("shorten last segment", () => path.ExpectedDistance.Value = 150); AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 150))); + // see remarks in `GetSegmentEnds()` xmldoc (`SliderPath.PositionAt()` clamps progress to [0,1]). AddAssert("segment end positions not recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(new[] { positions[1], From 5009fd379421f928bca35028b00e6b88374bdb5e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 20 Aug 2023 17:57:45 +0900 Subject: [PATCH 1127/2100] Add test coverage of song bar crash --- .../Components/TestSceneSongBar.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs index 0f31192a9c..d52b453185 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs @@ -2,27 +2,34 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Beatmaps.Legacy; -using osu.Game.Tests.Visual; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; namespace osu.Game.Tournament.Tests.Components { [TestFixture] - public partial class TestSceneSongBar : OsuTestScene + public partial class TestSceneSongBar : TournamentTestScene { - [Cached] - private readonly LadderInfo ladder = new LadderInfo(); - private SongBar songBar = null!; [SetUpSteps] - public void SetUpSteps() + public override void SetUpSteps() { + base.SetUpSteps(); + + AddStep("setup picks bans", () => + { + Ladder.CurrentMatch.Value!.PicksBans.Add(new BeatmapChoice + { + BeatmapID = CreateSampleBeatmap().OnlineID, + Team = TeamColour.Red, + Type = ChoiceType.Pick, + }); + }); + AddStep("create bar", () => Child = songBar = new SongBar { RelativeSizeAxes = Axes.X, @@ -38,12 +45,14 @@ namespace osu.Game.Tournament.Tests.Components AddStep("set beatmap", () => { var beatmap = CreateAPIBeatmap(Ruleset.Value); + beatmap.CircleSize = 3.4f; beatmap.ApproachRate = 6.8f; beatmap.OverallDifficulty = 5.5f; beatmap.StarRating = 4.56f; beatmap.Length = 123456; beatmap.BPM = 133; + beatmap.OnlineID = CreateSampleBeatmap().OnlineID; songBar.Beatmap = new TournamentBeatmap(beatmap); }); From 1067769b24e924bb34d4cc8975e94e162c7198c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Aug 2023 15:33:58 +0900 Subject: [PATCH 1128/2100] Remove masking on song bar Turns out this breaks when a border style is applied for picks/bans, and it wasn't doing much for visuals anyway. --- osu.Game.Tournament/Components/SongBar.cs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index 3d060600f7..cde826628e 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -234,7 +234,7 @@ namespace osu.Game.Tournament.Components } } }, - new UnmaskedTournamentBeatmapPanel(beatmap) + new TournamentBeatmapPanel(beatmap) { RelativeSizeAxes = Axes.X, Width = 0.5f, @@ -277,18 +277,4 @@ namespace osu.Game.Tournament.Components } } } - - internal partial class UnmaskedTournamentBeatmapPanel : TournamentBeatmapPanel - { - public UnmaskedTournamentBeatmapPanel(IBeatmapInfo? beatmap, string mod = "") - : base(beatmap, mod) - { - } - - [BackgroundDependencyLoader] - private void load() - { - Masking = false; - } - } } From f03c64462e5bd6a21626e7030cfaaf13a50c2e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Aug 2023 08:58:49 +0200 Subject: [PATCH 1129/2100] Better convey meaning of zero last year placement via tooltip --- osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index 241692d515..250d5acaae 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -10,7 +10,9 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Settings; using osu.Game.Tournament.Models; @@ -128,7 +130,7 @@ namespace osu.Game.Tournament.Screens.Editors Width = 0.2f, Current = Model.Seed }, - new SettingsSlider + new SettingsSlider { LabelText = "Last Year Placement", Width = 0.33f, @@ -175,6 +177,11 @@ namespace osu.Game.Tournament.Screens.Editors }; } + private partial class LastYearPlacementSlider : RoundedSliderBar + { + public override LocalisableString TooltipText => Current.Value == 0 ? "N/A" : base.TooltipText; + } + public partial class PlayerEditor : CompositeDrawable { private readonly TournamentTeam team; From 827d48adcc5fb3d334c5175879898b604871b4d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Aug 2023 16:10:48 +0900 Subject: [PATCH 1130/2100] Fix test coverage not actually covering crash --- osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs index d52b453185..e0444b6126 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs @@ -14,6 +14,7 @@ namespace osu.Game.Tournament.Tests.Components public partial class TestSceneSongBar : TournamentTestScene { private SongBar songBar = null!; + private TournamentBeatmap ladderBeatmap = null!; [SetUpSteps] public override void SetUpSteps() @@ -22,9 +23,10 @@ namespace osu.Game.Tournament.Tests.Components AddStep("setup picks bans", () => { + ladderBeatmap = CreateSampleBeatmap(); Ladder.CurrentMatch.Value!.PicksBans.Add(new BeatmapChoice { - BeatmapID = CreateSampleBeatmap().OnlineID, + BeatmapID = ladderBeatmap.OnlineID, Team = TeamColour.Red, Type = ChoiceType.Pick, }); @@ -52,7 +54,7 @@ namespace osu.Game.Tournament.Tests.Components beatmap.StarRating = 4.56f; beatmap.Length = 123456; beatmap.BPM = 133; - beatmap.OnlineID = CreateSampleBeatmap().OnlineID; + beatmap.OnlineID = ladderBeatmap.OnlineID; songBar.Beatmap = new TournamentBeatmap(beatmap); }); From e7d61e00022a4a09e8164eb809e2a1dc6b34dfa6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Aug 2023 17:59:24 +0900 Subject: [PATCH 1131/2100] Fix star fountain directions not matching stable --- .../Visual/Menus/TestSceneStarFountain.cs | 2 +- osu.Game/Screens/Menu/KiaiMenuFountains.cs | 30 +++++++++++++++---- osu.Game/Screens/Menu/StarFountain.cs | 6 ++-- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs index b12f3e7946..bb327e5962 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Menus foreach (var fountain in Children.OfType()) { if (RNG.NextSingle() > 0.8f) - fountain.Shoot(); + fountain.Shoot(RNG.Next(-1, 2)); } }, 150); } diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs index a4d58e398a..07c06dcdb9 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -2,10 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Graphics; +using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; @@ -13,6 +13,9 @@ namespace osu.Game.Screens.Menu { public partial class KiaiMenuFountains : BeatSyncedContainer { + private StarFountain leftFountain = null!; + private StarFountain rightFountain = null!; + [BackgroundDependencyLoader] private void load() { @@ -20,13 +23,13 @@ namespace osu.Game.Screens.Menu Children = new[] { - new StarFountain + leftFountain = new StarFountain { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, X = 250, }, - new StarFountain + rightFountain = new StarFountain { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, @@ -58,8 +61,25 @@ namespace osu.Game.Screens.Menu if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500) return; - foreach (var fountain in Children.OfType()) - fountain.Shoot(); + int direction = RNG.Next(-1, 2); + + switch (direction) + { + case -1: + leftFountain.Shoot(1); + rightFountain.Shoot(-1); + break; + + case 0: + leftFountain.Shoot(0); + rightFountain.Shoot(0); + break; + + case 1: + leftFountain.Shoot(-1); + rightFountain.Shoot(1); + break; + } lastTrigger = Clock.CurrentTime; } diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs index 0d35f6e0e0..fd59ec3573 100644 --- a/osu.Game/Screens/Menu/StarFountain.cs +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Menu InternalChild = spewer = new StarFountainSpewer(); } - public void Shoot() => spewer.Shoot(); + public void Shoot(int direction) => spewer.Shoot(direction); protected override void SkinChanged(ISkinSource skin) { @@ -81,10 +81,10 @@ namespace osu.Game.Screens.Menu return lastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / shoot_duration) + getRandomVariance(x_velocity_random_variance); } - public void Shoot() + public void Shoot(int direction) { lastShootTime = Clock.CurrentTime; - lastShootDirection = RNG.Next(-1, 2); + lastShootDirection = direction; } private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance); From 5f040a991b3f1492c97d247799a3245b2b825ee1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Aug 2023 19:05:20 +0900 Subject: [PATCH 1132/2100] Fix potential crash when loading menu items due to cross-thread ops --- osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs index eb046932e6..2f2cb7e5f8 100644 --- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs @@ -40,8 +40,14 @@ namespace osu.Game.Graphics.UserInterface AddInternal(hoverClickSounds = new HoverClickSounds()); updateTextColour(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); Item.Action.BindDisabledChanged(_ => updateState(), true); + FinishTransforms(); } private void updateTextColour() From 662073c47220d513ed3ef510d70ab0ccb850e334 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Aug 2023 19:35:04 +0900 Subject: [PATCH 1133/2100] Fix some incorrect comments / test step descriptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs | 2 +- osu.Game/Scoring/ScoreInfo.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index 23b88b7395..f766faec9a 100644 --- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -132,7 +132,7 @@ namespace osu.Game.Tests.Database { ScoreInfo scoreInfo = null!; - AddStep("Add score which requires upgrade (but has no beatmap)", () => + AddStep("Add score which requires upgrade (and has beatmap)", () => { Realm.Write(r => { diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index c3a45332e4..526c4217ef 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -86,7 +86,7 @@ namespace osu.Game.Scoring /// Should be used to ensure we don't repeatedly attempt to update the same scores each startup even though we already know they will fail. /// /// - /// See https://github.com/ppy/osu/issues/24301 for one example of how this can occur(missing beatmap file on disk). + /// See https://github.com/ppy/osu/issues/24301 for one example of how this can occur (missing beatmap file on disk). /// public bool TotalScoreUpgradeFailed { get; set; } From b3e7416972f097242bf8c9fe407dec3567d489d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Aug 2023 19:36:22 +0900 Subject: [PATCH 1134/2100] Rename new flag and update xmldoc to match --- .../Database/BackgroundDataStoreProcessorTests.cs | 4 ++-- osu.Game/BackgroundDataStoreProcessor.cs | 8 ++++---- osu.Game/Scoring/ScoreInfo.cs | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index f766faec9a..da46392e4b 100644 --- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -148,7 +148,7 @@ namespace osu.Game.Tests.Database AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor())); AddUntilStep("Score version upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(LegacyScoreEncoder.LATEST_VERSION)); - AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreUpgradeFailed), () => Is.False); + AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False); } [Test] @@ -174,7 +174,7 @@ namespace osu.Game.Tests.Database AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor())); - AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreUpgradeFailed), () => Is.True); + AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True); AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000002)); } diff --git a/osu.Game/BackgroundDataStoreProcessor.cs b/osu.Game/BackgroundDataStoreProcessor.cs index ae9e9527de..f29b100ee8 100644 --- a/osu.Game/BackgroundDataStoreProcessor.cs +++ b/osu.Game/BackgroundDataStoreProcessor.cs @@ -186,7 +186,7 @@ namespace osu.Game realmAccess.Run(r => { - foreach (var score in r.All().Where(s => !s.TotalScoreUpgradeFailed)) + foreach (var score in r.All().Where(s => !s.BackgroundReprocessingFailed)) { if (score.BeatmapInfo != null && score.Statistics.Sum(kvp => kvp.Value) > 0 @@ -225,7 +225,7 @@ namespace osu.Game catch (Exception e) { Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}"); - realmAccess.Write(r => r.Find(id)!.TotalScoreUpgradeFailed = true); + realmAccess.Write(r => r.Find(id)!.BackgroundReprocessingFailed = true); } } } @@ -235,7 +235,7 @@ namespace osu.Game Logger.Log("Querying for scores that need total score conversion..."); HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All() - .Where(s => !s.TotalScoreUpgradeFailed && s.BeatmapInfo != null && s.TotalScoreVersion == 30000002) + .Where(s => !s.BackgroundReprocessingFailed && s.BeatmapInfo != null && s.TotalScoreVersion == 30000002) .AsEnumerable().Select(s => s.ID))); Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); @@ -284,7 +284,7 @@ namespace osu.Game catch (Exception e) { Logger.Log($"Failed to convert total score for {id}: {e}"); - realmAccess.Write(r => r.Find(id)!.TotalScoreUpgradeFailed = true); + realmAccess.Write(r => r.Find(id)!.BackgroundReprocessingFailed = true); ++failedCount; } } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 526c4217ef..2efea2105c 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -82,13 +82,13 @@ namespace osu.Game.Scoring public long? LegacyTotalScore { get; set; } /// - /// If an reprocess of total score failed to update this score to the latest version, this flag will become true. - /// Should be used to ensure we don't repeatedly attempt to update the same scores each startup even though we already know they will fail. + /// If background processing of this beatmap failed in some way, this flag will become true. + /// Should be used to ensure we don't repeatedly attempt to reprocess the same scores each startup even though we already know they will fail. /// /// /// See https://github.com/ppy/osu/issues/24301 for one example of how this can occur (missing beatmap file on disk). /// - public bool TotalScoreUpgradeFailed { get; set; } + public bool BackgroundReprocessingFailed { get; set; } public int MaxCombo { get; set; } From 82de7385d1c07dc0bd3e21a46ee6220e82a59244 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 21 Aug 2023 12:59:58 +0200 Subject: [PATCH 1135/2100] Revert "Fix TestSceneFruitRandomness" This reverts commit b9d0a8a9f69fadacf1d1578fccd9101798583d47. --- osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs index 8e7f77285c..de3d9d6530 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -26,8 +26,6 @@ namespace osu.Game.Rulesets.Catch.Tests AddSliderStep("start time", 500, 600, 0, x => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = x; - drawableFruit.RefreshStateTransforms(); - drawableBanana.RefreshStateTransforms(); }); } @@ -46,8 +44,6 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("Initialize start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; - drawableFruit.RefreshStateTransforms(); - drawableBanana.RefreshStateTransforms(); fruitRotation = drawableFruit.DisplayRotation; bananaRotation = drawableBanana.DisplayRotation; @@ -58,8 +54,6 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("change start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = another_start_time; - drawableFruit.RefreshStateTransforms(); - drawableBanana.RefreshStateTransforms(); }); AddAssert("fruit rotation is changed", () => drawableFruit.DisplayRotation != fruitRotation); @@ -70,8 +64,6 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("reset start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; - drawableFruit.RefreshStateTransforms(); - drawableBanana.RefreshStateTransforms(); }); AddAssert("rotation and size restored", () => From c7b1c75379983860a2dec8bd2de92d4331a0fc75 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 21 Aug 2023 13:00:01 +0200 Subject: [PATCH 1136/2100] Revert "Fix typo" This reverts commit 90f2acaf0a9d1bfa5ac8d4cc653798604a3fdf21. --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 76cfc049e3..e919e4d088 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -189,7 +189,7 @@ namespace osu.Game.Rulesets.UI private void unbindUpdated(DrawableHitObject hitObject) { - hitObject.DefaultsApplied -= onDefaultsApplied; + hitObject.DefaultsApplied += onDefaultsApplied; } private void unbindAllUpdated() From 5bc11ed35806ac8fac87c82118ff904905df3127 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 21 Aug 2023 13:02:23 +0200 Subject: [PATCH 1137/2100] Revert "Ensure invariant of monotone time" This reverts commit 5d1ccc2601a739e2d8ed61b8d9b64a0efa580f7f. --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 30 +------------------ osu.Game/Rulesets/UI/Playfield.cs | 35 ++++------------------ 2 files changed, 7 insertions(+), 58 deletions(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index e919e4d088..099be486b3 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -51,11 +51,6 @@ namespace osu.Game.Rulesets.UI [Resolved(CanBeNull = true)] private IPooledHitObjectProvider pooledObjectProvider { get; set; } - /// - /// Invoked when a is updated. - /// - public event Action HitObjectUpdated; - public HitObjectContainer() { RelativeSizeAxes = Axes.Both; @@ -113,7 +108,6 @@ namespace osu.Game.Rulesets.UI drawable.OnNewResult += onNewResult; bindStartTime(drawable); - bindUpdated(drawable); AddInternal(drawable); } @@ -122,7 +116,7 @@ namespace osu.Game.Rulesets.UI drawable.OnNewResult -= onNewResult; unbindStartTime(drawable); - unbindUpdated(drawable); + RemoveInternal(drawable, false); } @@ -182,27 +176,6 @@ namespace osu.Game.Rulesets.UI startTimeMap.Clear(); } - private void bindUpdated(DrawableHitObject hitObject) - { - hitObject.DefaultsApplied += onDefaultsApplied; - } - - private void unbindUpdated(DrawableHitObject hitObject) - { - hitObject.DefaultsApplied += onDefaultsApplied; - } - - private void unbindAllUpdated() - { - foreach (var h in AliveObjects) - unbindUpdated(h); - } - - private void onDefaultsApplied(DrawableHitObject obj) - { - HitObjectUpdated?.Invoke(obj.HitObject); - } - protected override int Compare(Drawable x, Drawable y) { if (!(x is DrawableHitObject xObj) || !(y is DrawableHitObject yObj)) @@ -219,7 +192,6 @@ namespace osu.Game.Rulesets.UI { base.Dispose(isDisposing); unbindAllStartTimes(); - unbindAllUpdated(); } } } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 9a4d082a06..e9c35555c8 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.UI private readonly HitObjectEntryManager entryManager = new HitObjectEntryManager(); - private readonly LinkedList judgedEntries; + private readonly Stack judgedEntries; /// /// Creates a new . @@ -125,13 +125,12 @@ namespace osu.Game.Rulesets.UI h.NewResult += onNewResult; h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o); h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o); - h.HitObjectUpdated += onHitObjectUpdated; })); entryManager.OnEntryAdded += onEntryAdded; entryManager.OnEntryRemoved += onEntryRemoved; - judgedEntries = new LinkedList(); + judgedEntries = new Stack(); } [BackgroundDependencyLoader] @@ -271,16 +270,15 @@ namespace osu.Game.Rulesets.UI } // When rewinding, revert future judgements in the reverse order. - while (judgedEntries.Last is not null) + while (judgedEntries.Count > 0) { - var result = judgedEntries.Last.Value.Result; + var result = judgedEntries.Peek().Result; Debug.Assert(result?.RawTime != null); if (Time.Current >= result.RawTime.Value) break; - revertResult(judgedEntries.Last.Value); - judgedEntries.RemoveLast(); + revertResult(judgedEntries.Pop()); } } @@ -473,31 +471,10 @@ namespace osu.Game.Rulesets.UI #endregion - private void onHitObjectUpdated(HitObject _) - { - // The time of judged entries may have changed, so we need to re-sort the list to preserve the invariant of monotone time. - // Insertion sort on linked-list is O(n) for nearly-sorted lists, which is the case here. - var current = judgedEntries.First; - - while (current?.Next is not null) - { - var next = current.Next; - - if (current.Value.Result?.RawTime > next.Value.Result?.RawTime) - { - judgedEntries.Remove(next); - judgedEntries.AddBefore(current, next); - current = next.Previous; - } - else - current = next; - } - } - private void onNewResult(DrawableHitObject drawable, JudgementResult result) { Debug.Assert(result != null && drawable.Entry?.Result == result && result.RawTime != null); - judgedEntries.AddLast(drawable.Entry.AsNonNull()); + judgedEntries.Push(drawable.Entry.AsNonNull()); NewResult?.Invoke(drawable, result); } From c82e997644529adb475bec516ead328d260c1fda Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 21 Aug 2023 13:02:41 +0200 Subject: [PATCH 1138/2100] Revert "Revert "Fix TestSceneFruitRandomness"" This reverts commit 82de7385d1c07dc0bd3e21a46ee6220e82a59244. --- osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs index de3d9d6530..8e7f77285c 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddSliderStep("start time", 500, 600, 0, x => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = x; + drawableFruit.RefreshStateTransforms(); + drawableBanana.RefreshStateTransforms(); }); } @@ -44,6 +46,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("Initialize start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; + drawableFruit.RefreshStateTransforms(); + drawableBanana.RefreshStateTransforms(); fruitRotation = drawableFruit.DisplayRotation; bananaRotation = drawableBanana.DisplayRotation; @@ -54,6 +58,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("change start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = another_start_time; + drawableFruit.RefreshStateTransforms(); + drawableBanana.RefreshStateTransforms(); }); AddAssert("fruit rotation is changed", () => drawableFruit.DisplayRotation != fruitRotation); @@ -64,6 +70,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("reset start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; + drawableFruit.RefreshStateTransforms(); + drawableBanana.RefreshStateTransforms(); }); AddAssert("rotation and size restored", () => From e283aa2843104bf58bf027486cee0d595c65fa3a Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 21 Aug 2023 13:09:31 +0200 Subject: [PATCH 1139/2100] Update inline comments --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index f141263e27..c442fac0b8 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -330,7 +330,7 @@ namespace osu.Game.Rulesets.Objects.Drawables ClearNestedHitObjects(); // Changes in state trigger defaults applied trigger state updates. - // When a new hitobject is applied, OnApply() automatically performs a state update anyway. + // When a new hitobject is applied, OnApply() automatically performs a state update. HitObject.DefaultsApplied -= onDefaultsApplied; entry.RevertResult -= onRevertResult; @@ -391,6 +391,8 @@ namespace osu.Game.Rulesets.Objects.Drawables Apply(Entry); // Applied defaults indicate a change in hit object state. + // We need to update the judgement result time to the new end time + // and update state to ensure the hit object fades out at the correct time. if (Result is not null) { Result.TimeOffset = 0; From bdac05263164d51af00fa23d4fe84f1e1fcf905c Mon Sep 17 00:00:00 2001 From: tsrk Date: Mon, 21 Aug 2023 15:29:41 +0200 Subject: [PATCH 1140/2100] refactor(MessageNotifier): apply changes required by framework --- osu.Game/Online/Chat/MessageNotifier.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index b91cf06847..de38d3ef26 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -127,7 +127,7 @@ namespace osu.Game.Online.Chat if (!notifyOnPrivateMessage.Value || channel.Type != ChannelType.PM) return false; - (host as DesktopGameHost)?.FlashWindow(); + host.Window?.Flash(); notifications.Post(new PrivateMessageNotification(message, channel)); return true; @@ -137,7 +137,7 @@ namespace osu.Game.Online.Chat { if (!notifyOnUsername.Value || !CheckContainsUsername(message.Content, localUser.Value.Username)) return; - (host as DesktopGameHost)?.FlashWindow(); + host.Window?.Flash(); notifications.Post(new MentionNotification(message, channel)); } From 8533cba0bf6a02b05b8e9bfe99aaee2822dfe5a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Aug 2023 17:27:05 +0200 Subject: [PATCH 1141/2100] Fix mismatching schema version in comment --- osu.Game/Database/RealmAccess.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 36781b0454..db4f0d9864 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -83,7 +83,7 @@ namespace osu.Game.Database /// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores. /// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files. /// 33 2023-08-16 Reset default chat toggle key binding to avoid conflict with newly added leaderboard toggle key binding. - /// 35 2023-08-21 Add TotalScoreUpgradeFailed flag to ScoreInfo to track upgrade failures. + /// 34 2023-08-21 Add TotalScoreUpgradeFailed flag to ScoreInfo to track upgrade failures. /// private const int schema_version = 34; From 273dcf9150f441c475c524455b12376a96d3930f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Aug 2023 17:44:35 +0200 Subject: [PATCH 1142/2100] Also update the reference to added flag in schema change breakdown --- osu.Game/Database/RealmAccess.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index db4f0d9864..cd97bb6430 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -83,7 +83,7 @@ namespace osu.Game.Database /// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores. /// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files. /// 33 2023-08-16 Reset default chat toggle key binding to avoid conflict with newly added leaderboard toggle key binding. - /// 34 2023-08-21 Add TotalScoreUpgradeFailed flag to ScoreInfo to track upgrade failures. + /// 34 2023-08-21 Add BackgroundReprocessingFailed flag to ScoreInfo to track upgrade failures. /// private const int schema_version = 34; From 5454d1caa1927428fe339e3d5b47ddfe45f9dfb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Aug 2023 17:40:55 +0200 Subject: [PATCH 1143/2100] Remove global action container input queue workaround As described in #24248, the workaround employed by `GlobalActionContainer`, wherein it tried to handle actions with priority before its children by being placed in front of the children and not _actually containing_ said children, is blocking the resolution of some rather major input handling issues that allow key releases to be received by deparented drawables. To resolve, migrate `GlobalActionContainer` to use `Prioritised`, which can be done without regressing certain mouse button flows after ppy/osu-framework#5966. --- .../Input/Bindings/GlobalActionContainer.cs | 34 +++++-------------- osu.Game/OsuGameBase.cs | 19 ++++++----- .../Visual/OsuManualInputManagerTestScene.cs | 8 ++++- 3 files changed, 25 insertions(+), 36 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 9a0a2d5c15..296232d9ea 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -3,33 +3,26 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Localisation; namespace osu.Game.Input.Bindings { - public partial class GlobalActionContainer : DatabasedKeyBindingContainer, IHandleGlobalKeyboardInput + public partial class GlobalActionContainer : DatabasedKeyBindingContainer, IHandleGlobalKeyboardInput, IKeyBindingHandler { - private readonly Drawable? handler; - - private InputManager? parentInputManager; + private readonly IKeyBindingHandler? handler; public GlobalActionContainer(OsuGameBase? game) : base(matchingMode: KeyCombinationMatchingMode.Modifiers) { - if (game is IKeyBindingHandler) - handler = game; + if (game is IKeyBindingHandler h) + handler = h; } - protected override void LoadComplete() - { - base.LoadComplete(); - - parentInputManager = GetContainingInputManager(); - } + protected override bool Prioritised => true; // IMPORTANT: Take care when changing order of the items in the enumerable. // It is used to decide the order of precedence, with the earlier items having higher precedence. @@ -161,20 +154,9 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.F3, GlobalAction.MusicPlay) }; - protected override IEnumerable KeyBindingInputQueue - { - get - { - // To ensure the global actions are handled with priority, this GlobalActionContainer is actually placed after game content. - // It does not contain children as expected, so we need to forward the NonPositionalInputQueue from the parent input manager to correctly - // allow the whole game to handle these actions. + public bool OnPressed(KeyBindingPressEvent e) => handler?.OnPressed(e) == true; - // An eventual solution to this hack is to create localised action containers for individual components like SongSelect, but this will take some rearranging. - var inputQueue = parentInputManager?.NonPositionalInputQueue ?? base.KeyBindingInputQueue; - - return handler != null ? inputQueue.Prepend(handler) : inputQueue; - } - } + public void OnReleased(KeyBindingReleaseEvent e) => handler?.OnReleased(e); } public enum GlobalAction diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 6737caa5f9..75b46a0a4d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -392,17 +392,18 @@ namespace osu.Game { SafeAreaOverrideEdges = SafeAreaOverrideEdges, RelativeSizeAxes = Axes.Both, - Child = CreateScalingContainer().WithChildren(new Drawable[] + Child = CreateScalingContainer().WithChild(globalBindings = new GlobalActionContainer(this) { - (GlobalCursorDisplay = new GlobalCursorDisplay + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both - }).WithChild(content = new OsuTooltipContainer(GlobalCursorDisplay.MenuCursor) - { - RelativeSizeAxes = Axes.Both - }), - // to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything. - globalBindings = new GlobalActionContainer(this) + (GlobalCursorDisplay = new GlobalCursorDisplay + { + RelativeSizeAxes = Axes.Both + }).WithChild(content = new OsuTooltipContainer(GlobalCursorDisplay.MenuCursor) + { + RelativeSizeAxes = Axes.Both + }), + } }) }); diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index 37260b3b13..ffe40243ab 100644 --- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs @@ -57,7 +57,13 @@ namespace osu.Game.Tests.Visual } if (CreateNestedActionContainer) - mainContent.Add(new GlobalActionContainer(null)); + { + var globalActionContainer = new GlobalActionContainer(null) + { + Child = mainContent + }; + mainContent = globalActionContainer; + } base.Content.AddRange(new Drawable[] { From 9f4f81c150895ddc08bb4680bbcbd981a03f9d0d Mon Sep 17 00:00:00 2001 From: Wleter Date: Mon, 21 Aug 2023 19:36:11 +0200 Subject: [PATCH 1144/2100] accumulating negative scaling --- .../SkinEditor/SkinSelectionHandler.cs | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index a952cf3035..c90a1d8edf 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -31,6 +31,8 @@ namespace osu.Game.Overlays.SkinEditor UpdatePosition = updateDrawablePosition }; + private float accumulatedNegativeScaling; + public override bool HandleScale(Vector2 scale, Anchor anchor) { // convert scale to screen space @@ -71,8 +73,25 @@ namespace osu.Game.Overlays.SkinEditor scale.Y = scale.X / selectionRect.Width * selectionRect.Height; } - // If scaling reverses the selection, don't scale. - if (adjustedRect.Width + scale.X < 0 || adjustedRect.Height + scale.Y < 0) return true; + // If scaling reverses the selection, don't scale and accumulate the amount of scaling. + if (adjustedRect.Width + scale.X < 0 || adjustedRect.Height + scale.Y < 0) + { + accumulatedNegativeScaling += scale.Length; // - new Vector2(selectionRect.Width, selectionRect.Height).Length; + + return true; + } + + // Compensate for accumulated negative scaling. + if (Precision.AlmostBigger(accumulatedNegativeScaling, 0) && !Precision.AlmostEquals(accumulatedNegativeScaling, 0)) + { + float length = scale.Length; + accumulatedNegativeScaling -= length; + + // If the accumulated negative scaling is still positive, don't scale. + if (Precision.AlmostBigger(accumulatedNegativeScaling, 0)) return true; + scale *= Math.Abs(accumulatedNegativeScaling) / length; + accumulatedNegativeScaling = 0; + } if (anchor.HasFlagFast(Anchor.x0)) adjustedRect.X -= scale.X; if (anchor.HasFlagFast(Anchor.y0)) adjustedRect.Y -= scale.Y; @@ -150,6 +169,12 @@ namespace osu.Game.Overlays.SkinEditor public static void ApplyClosestAnchor(Drawable drawable) => applyAnchor(drawable, getClosestAnchor(drawable)); + protected override void OnOperationEnded() + { + base.OnOperationEnded(); + accumulatedNegativeScaling = 0; + } + protected override void OnSelectionChanged() { base.OnSelectionChanged(); From 96c58c86ea85c1e2e85cb578a730be649993351d Mon Sep 17 00:00:00 2001 From: tsrk Date: Mon, 21 Aug 2023 23:36:54 +0200 Subject: [PATCH 1145/2100] refactor: make flashing available in `Notifications` This will be used in `NotificationOverlay` when a `Notification` is posted. --- osu.Game/Online/Chat/MessageNotifier.cs | 4 ---- osu.Game/Overlays/NotificationOverlay.cs | 9 +++++++++ osu.Game/Overlays/Notifications/Notification.cs | 5 +++++ osu.Game/Screens/Play/PlayerLoader.cs | 2 ++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index de38d3ef26..65aac723da 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -127,8 +127,6 @@ namespace osu.Game.Online.Chat if (!notifyOnPrivateMessage.Value || channel.Type != ChannelType.PM) return false; - host.Window?.Flash(); - notifications.Post(new PrivateMessageNotification(message, channel)); return true; } @@ -137,8 +135,6 @@ namespace osu.Game.Online.Chat { if (!notifyOnUsername.Value || !CheckContainsUsername(message.Content, localUser.Value.Username)) return; - host.Window?.Flash(); - notifications.Post(new MentionNotification(message, channel)); } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index c9d09848f8..08c567af82 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -43,6 +43,9 @@ namespace osu.Game.Overlays [Resolved] private AudioManager audio { get; set; } = null!; + [Resolved] + private OsuGame game { get; set; } = null!; + [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); @@ -176,6 +179,12 @@ namespace osu.Game.Overlays playDebouncedSample(notification.PopInSampleName); + if (notification.FlashTaskbar) + { + game.Window?.Flash(notification.IsImportant); + notification.Closed += () => game.Window?.CancelFlash(); + } + if (State.Value == Visibility.Hidden) { notification.IsInToastTray = true; diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 8cdc373417..53fc152c96 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -38,6 +38,11 @@ namespace osu.Game.Overlays.Notifications /// public virtual bool IsImportant => true; + /// + /// Whether this notification should trigger a taskbar flash if the window is un-focused when posted. + /// + public bool FlashTaskbar { get; init; } = true; + /// /// Run on user activating the notification. Return true to close. /// diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 872425e3fd..eccfc4dc7b 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -568,6 +568,7 @@ namespace osu.Game.Screens.Play public MutedNotification() { Text = NotificationsStrings.GameVolumeTooLow; + FlashTaskbar = false; } [BackgroundDependencyLoader] @@ -623,6 +624,7 @@ namespace osu.Game.Screens.Play public BatteryWarningNotification() { Text = NotificationsStrings.BatteryLow; + FlashTaskbar = false; } [BackgroundDependencyLoader] From e321303ef665a623436a9bf0e04cf91aebd3323b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Aug 2023 12:32:42 +0900 Subject: [PATCH 1146/2100] Add application category type to enable game mode on new macOS versions --- osu.iOS/Info.plist | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 0ce1d952d0..cf51fe995b 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -34,9 +34,9 @@ CADisableMinimumFrameDurationOnPhone NSCameraUsageDescription - We don't really use the camera. + We don't really use the camera. NSMicrophoneUsageDescription - We don't really use the microphone. + We don't really use the microphone. UISupportedInterfaceOrientations UIInterfaceOrientationLandscapeRight @@ -130,5 +130,7 @@ Editor + LSApplicationCategoryType + public.app-category.music-games From e8337c592a69e1905e75d459bf5ff6aad6c0d47f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 22 Aug 2023 12:50:13 +0900 Subject: [PATCH 1147/2100] Update framework and apply changes to support masking SSBO --- .../UI/Cursor/CursorTrail.cs | 20 ++++++++++++++----- .../Resources/Shaders/sh_TestVertex.vs | 2 +- osu.Game/Screens/Loader.cs | 5 +---- osu.Game/osu.Game.csproj | 2 +- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index a29faac5a0..0774d34488 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -286,7 +286,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor if (time - part.Time >= 1) continue; - vertexBatch.Add(new TexturedTrailVertex + vertexBatch.Add(new TexturedTrailVertex(renderer) { Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)), TexturePosition = textureRect.BottomLeft, @@ -295,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Time = part.Time }); - vertexBatch.Add(new TexturedTrailVertex + vertexBatch.Add(new TexturedTrailVertex(renderer) { Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)), TexturePosition = textureRect.BottomRight, @@ -304,7 +304,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Time = part.Time }); - vertexBatch.Add(new TexturedTrailVertex + vertexBatch.Add(new TexturedTrailVertex(renderer) { Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y), TexturePosition = textureRect.TopRight, @@ -313,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Time = part.Time }); - vertexBatch.Add(new TexturedTrailVertex + vertexBatch.Add(new TexturedTrailVertex(renderer) { Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y), TexturePosition = textureRect.TopLeft, @@ -362,12 +362,22 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor [VertexMember(1, VertexAttribPointerType.Float)] public float Time; + [VertexMember(1, VertexAttribPointerType.Int)] + private readonly int maskingIndex; + + public TexturedTrailVertex(IRenderer renderer) + { + this = default; + maskingIndex = renderer.CurrentMaskingIndex; + } + public bool Equals(TexturedTrailVertex other) { return Position.Equals(other.Position) && TexturePosition.Equals(other.TexturePosition) && Colour.Equals(other.Colour) - && Time.Equals(other.Time); + && Time.Equals(other.Time) + && maskingIndex == other.maskingIndex; } } } diff --git a/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs b/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs index 505554bb33..80ed686ba5 100644 --- a/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs +++ b/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs @@ -13,7 +13,7 @@ layout(location = 4) out mediump vec2 v_BlendRange; void main(void) { // Transform from screen space to masking space. - highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0); + highp vec3 maskingPos = g_MaskingInfo.ToMaskingSpace * vec3(m_Position, 1.0); v_MaskingPosition = maskingPos.xy / maskingPos.z; v_Colour = m_Colour; diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 372cfe748e..962c7d9d14 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -126,12 +126,9 @@ namespace osu.Game.Screens private void load(ShaderManager manager) { loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE)); - loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR)); - + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2_NO_MASKING, FragmentShaderDescriptor.BLUR)); loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE)); - loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder")); - loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7d4a721c91..08107c2fad 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + From f09b81841810df3e63a9bc6bfa11a369803f0d89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Aug 2023 13:17:12 +0900 Subject: [PATCH 1148/2100] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 08107c2fad..4c3205178a 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From be1a712f33bfa755a049d9b2763a22f45f8a1498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Aug 2023 08:54:41 +0200 Subject: [PATCH 1149/2100] Make `OsuGame` dependency nullable --- osu.Game/Overlays/NotificationOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 08c567af82..6dd344ca99 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -44,7 +44,7 @@ namespace osu.Game.Overlays private AudioManager audio { get; set; } = null!; [Resolved] - private OsuGame game { get; set; } = null!; + private OsuGame? game { get; set; } [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); @@ -181,8 +181,8 @@ namespace osu.Game.Overlays if (notification.FlashTaskbar) { - game.Window?.Flash(notification.IsImportant); - notification.Closed += () => game.Window?.CancelFlash(); + game?.Window?.Flash(notification.IsImportant); + notification.Closed += () => game?.Window?.CancelFlash(); } if (State.Value == Visibility.Hidden) From aa29e00578a01768d1ee7bf537e5b2a335c46b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Aug 2023 08:58:10 +0200 Subject: [PATCH 1150/2100] Remove `FlashTaskbar` and use `IsImportant` directly instead --- osu.Game/Overlays/NotificationOverlay.cs | 4 ++-- osu.Game/Overlays/Notifications/Notification.cs | 5 ----- osu.Game/Screens/Play/PlayerLoader.cs | 2 -- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 6dd344ca99..b93d5f1e12 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -179,9 +179,9 @@ namespace osu.Game.Overlays playDebouncedSample(notification.PopInSampleName); - if (notification.FlashTaskbar) + if (notification.IsImportant) { - game?.Window?.Flash(notification.IsImportant); + game?.Window?.Flash(); notification.Closed += () => game?.Window?.CancelFlash(); } diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 53fc152c96..8cdc373417 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -38,11 +38,6 @@ namespace osu.Game.Overlays.Notifications /// public virtual bool IsImportant => true; - /// - /// Whether this notification should trigger a taskbar flash if the window is un-focused when posted. - /// - public bool FlashTaskbar { get; init; } = true; - /// /// Run on user activating the notification. Return true to close. /// diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index eccfc4dc7b..872425e3fd 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -568,7 +568,6 @@ namespace osu.Game.Screens.Play public MutedNotification() { Text = NotificationsStrings.GameVolumeTooLow; - FlashTaskbar = false; } [BackgroundDependencyLoader] @@ -624,7 +623,6 @@ namespace osu.Game.Screens.Play public BatteryWarningNotification() { Text = NotificationsStrings.BatteryLow; - FlashTaskbar = false; } [BackgroundDependencyLoader] From 142abe1fd0a87a2144ee4f61c40cff1c96ce4605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Aug 2023 09:00:59 +0200 Subject: [PATCH 1151/2100] Make highlight messages important in order to trigger window flash --- osu.Game/Online/Chat/MessageNotifier.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 65aac723da..56f490cb21 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -182,8 +182,6 @@ namespace osu.Game.Online.Chat private readonly Message message; private readonly Channel channel; - public override bool IsImportant => false; - [BackgroundDependencyLoader] private void load(OsuColour colours, ChatOverlay chatOverlay, INotificationOverlay notificationOverlay) { From 5be533578499d25c54e18ea76ec2aae2a32a9b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Aug 2023 09:37:54 +0200 Subject: [PATCH 1152/2100] Reword comment to be better --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index c442fac0b8..e31656e0ff 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -329,8 +329,8 @@ namespace osu.Game.Rulesets.Objects.Drawables Entry.NestedEntries.RemoveAll(nestedEntry => nestedEntry is SyntheticHitObjectEntry); ClearNestedHitObjects(); - // Changes in state trigger defaults applied trigger state updates. - // When a new hitobject is applied, OnApply() automatically performs a state update. + // Changes to `HitObject` properties trigger default application, which triggers `State` updates. + // When a new hitobject is applied, `OnApply()` automatically performs a state update. HitObject.DefaultsApplied -= onDefaultsApplied; entry.RevertResult -= onRevertResult; From 290d18ad690accc2a54c3290e3403f6c2534fb45 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Aug 2023 17:31:19 +0900 Subject: [PATCH 1153/2100] Split out difficulties in beatmap carousel in a bit of a hacky way Seems like the simplest path forward for now, without a full rewrite. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 82 ++++++++++++++++------ osu.Game/Screens/Select/FilterCriteria.cs | 5 ++ 2 files changed, 65 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 9af9a0ce72..d1a9b4176b 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -78,6 +78,8 @@ namespace osu.Game.Screens.Select private CarouselBeatmapSet? selectedBeatmapSet; + private IEnumerable originalBeatmapSetsDetached = Enumerable.Empty(); + /// /// Raised when the is changed. /// @@ -127,13 +129,29 @@ namespace osu.Game.Screens.Select private void loadBeatmapSets(IEnumerable beatmapSets) { + originalBeatmapSetsDetached = beatmapSets.Detach(); + CarouselRoot newRoot = new CarouselRoot(this); - newRoot.AddItems(beatmapSets.Select(s => createCarouselSet(s.Detach())).OfType()); + if (beatmapsSplitOut) + { + var carouselBeatmapSets = originalBeatmapSetsDetached.SelectMany(s => s.Beatmaps).Select(b => + { + var set = new BeatmapSetInfo(new[] { b }); + return createCarouselSet(set); + }).OfType(); + + newRoot.AddItems(carouselBeatmapSets); + } + else + { + var carouselBeatmapSets = originalBeatmapSetsDetached.Select(createCarouselSet).OfType(); + newRoot.AddItems(carouselBeatmapSets); + } root = newRoot; - if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet)) + if (selectedBeatmapSet != null && !originalBeatmapSetsDetached.Contains(selectedBeatmapSet.BeatmapSet)) selectedBeatmapSet = null; Scroll.Clear(false); @@ -330,8 +348,8 @@ namespace osu.Game.Screens.Select // Only require to action here if the beatmap is missing. // This avoids processing these events unnecessarily when new beatmaps are imported, for example. - if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSet) - && existingSet.BeatmapSet.Beatmaps.All(b => b.ID != beatmapInfo.ID)) + if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSets) + && existingSets.SelectMany(s => s.Beatmaps).All(b => b.BeatmapInfo.ID != beatmapInfo.ID)) { UpdateBeatmapSet(beatmapSet.Detach()); } @@ -345,15 +363,18 @@ namespace osu.Game.Screens.Select private void removeBeatmapSet(Guid beatmapSetID) => Schedule(() => { - if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSet)) + if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSets)) return; - foreach (var beatmap in existingSet.Beatmaps) - randomSelectedBeatmaps.Remove(beatmap); + foreach (var set in existingSets) + { + foreach (var beatmap in set.Beatmaps) + randomSelectedBeatmaps.Remove(beatmap); + previouslyVisitedRandomSets.Remove(set); - previouslyVisitedRandomSets.Remove(existingSet); + root.RemoveItem(set); + } - root.RemoveItem(existingSet); itemsCache.Invalidate(); if (!Scroll.UserScrolling) @@ -371,13 +392,16 @@ namespace osu.Game.Screens.Select previouslySelectedID = selectedBeatmap?.BeatmapInfo.ID; var newSet = createCarouselSet(beatmapSet); - var removedSet = root.RemoveChild(beatmapSet.ID); + var removedSets = root.RemoveChild(beatmapSet.ID); - // If we don't remove this here, it may remain in a hidden state until scrolled off screen. - // Doesn't really affect anything during actual user interaction, but makes testing annoying. - var removedDrawable = Scroll.FirstOrDefault(c => c.Item == removedSet); - if (removedDrawable != null) - expirePanelImmediately(removedDrawable); + foreach (var removedSet in removedSets) + { + // If we don't remove this here, it may remain in a hidden state until scrolled off screen. + // Doesn't really affect anything during actual user interaction, but makes testing annoying. + var removedDrawable = Scroll.FirstOrDefault(c => c.Item == removedSet); + if (removedDrawable != null) + expirePanelImmediately(removedDrawable); + } if (newSet != null) { @@ -632,6 +656,8 @@ namespace osu.Game.Screens.Select applyActiveCriteria(debounce); } + private bool beatmapsSplitOut; + private void applyActiveCriteria(bool debounce, bool alwaysResetScrollPosition = true) { PendingFilter?.Cancel(); @@ -652,6 +678,13 @@ namespace osu.Game.Screens.Select { PendingFilter = null; + if (activeCriteria.SplitOutDifficulties != beatmapsSplitOut) + { + beatmapsSplitOut = activeCriteria.SplitOutDifficulties; + loadBeatmapSets(originalBeatmapSetsDetached); + return; + } + root.Filter(activeCriteria); itemsCache.Invalidate(); @@ -1055,7 +1088,7 @@ namespace osu.Game.Screens.Select // May only be null during construction (State.Value set causes PerformSelection to be triggered). private readonly BeatmapCarousel? carousel; - public readonly Dictionary BeatmapSetsByID = new Dictionary(); + public readonly Dictionary> BeatmapSetsByID = new Dictionary>(); public CarouselRoot(BeatmapCarousel carousel) { @@ -1069,20 +1102,25 @@ namespace osu.Game.Screens.Select public override void AddItem(CarouselItem i) { CarouselBeatmapSet set = (CarouselBeatmapSet)i; - BeatmapSetsByID.Add(set.BeatmapSet.ID, set); + if (BeatmapSetsByID.TryGetValue(set.BeatmapSet.ID, out var sets)) + sets.Add(set); + else + BeatmapSetsByID.Add(set.BeatmapSet.ID, new List { set }); base.AddItem(i); } - public CarouselBeatmapSet? RemoveChild(Guid beatmapSetID) + public IEnumerable RemoveChild(Guid beatmapSetID) { - if (BeatmapSetsByID.TryGetValue(beatmapSetID, out var carouselBeatmapSet)) + if (BeatmapSetsByID.TryGetValue(beatmapSetID, out var carouselBeatmapSets)) { - RemoveItem(carouselBeatmapSet); - return carouselBeatmapSet; + foreach (var set in carouselBeatmapSets) + RemoveItem(set); + + return carouselBeatmapSets; } - return null; + return Enumerable.Empty(); } public override void RemoveItem(CarouselItem i) diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index ab4f85fc92..a2ae114126 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -19,6 +19,11 @@ namespace osu.Game.Screens.Select public GroupMode Group; public SortMode Sort; + /// + /// Whether the display of beatmap sets should be split apart per-difficulty for the current criteria. + /// + public bool SplitOutDifficulties => Sort == SortMode.Difficulty; + public BeatmapSetInfo? SelectedBeatmapSet; public OptionalRange StarDifficulty; From 2b1c6ae612cb4e6cecbd6736748da942fa724e39 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Aug 2023 18:38:23 +0900 Subject: [PATCH 1154/2100] Ensure ID is maintained in temporary `BeatmapSetInfo`s --- osu.Game/Screens/Select/BeatmapCarousel.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index d1a9b4176b..5157e37a31 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -137,8 +137,10 @@ namespace osu.Game.Screens.Select { var carouselBeatmapSets = originalBeatmapSetsDetached.SelectMany(s => s.Beatmaps).Select(b => { - var set = new BeatmapSetInfo(new[] { b }); - return createCarouselSet(set); + return createCarouselSet(new BeatmapSetInfo(new[] { b }) + { + ID = b.BeatmapSet!.ID, + }); }).OfType(); newRoot.AddItems(carouselBeatmapSets); From ecbf0f138e2c7b3cf5716020a1aad4f8a834dfd7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Aug 2023 18:38:43 +0900 Subject: [PATCH 1155/2100] Fix incorrect handling when new beatmaps arrive --- osu.Game/Screens/Select/BeatmapCarousel.cs | 35 ++++++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 5157e37a31..2227eb801a 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -393,7 +393,6 @@ namespace osu.Game.Screens.Select if (selectedBeatmapSet?.BeatmapSet.ID == beatmapSet.ID) previouslySelectedID = selectedBeatmap?.BeatmapInfo.ID; - var newSet = createCarouselSet(beatmapSet); var removedSets = root.RemoveChild(beatmapSet.ID); foreach (var removedSet in removedSets) @@ -405,13 +404,37 @@ namespace osu.Game.Screens.Select expirePanelImmediately(removedDrawable); } - if (newSet != null) + if (beatmapsSplitOut) { - root.AddItem(newSet); + foreach (var beatmap in beatmapSet.Beatmaps) + { + var newSet = createCarouselSet(new BeatmapSetInfo(new[] { beatmap }) + { + ID = beatmapSet.ID + }); - // check if we can/need to maintain our current selection. - if (previouslySelectedID != null) - select((CarouselItem?)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet); + if (newSet != null) + { + root.AddItem(newSet); + + // check if we can/need to maintain our current selection. + if (previouslySelectedID != null) + select((CarouselItem?)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet); + } + } + } + else + { + var newSet = createCarouselSet(beatmapSet); + + if (newSet != null) + { + root.AddItem(newSet); + + // check if we can/need to maintain our current selection. + if (previouslySelectedID != null) + select((CarouselItem?)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet); + } } itemsCache.Invalidate(); From 018be4c20f408ee98bb00a3897e2658b4f016596 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Aug 2023 18:40:34 +0900 Subject: [PATCH 1156/2100] Fix selection not being retained when switching between split mode --- osu.Game/Screens/Select/BeatmapCarousel.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 2227eb801a..c5e46a00b6 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -131,6 +131,12 @@ namespace osu.Game.Screens.Select { originalBeatmapSetsDetached = beatmapSets.Detach(); + if (selectedBeatmapSet != null && !originalBeatmapSetsDetached.Contains(selectedBeatmapSet.BeatmapSet)) + selectedBeatmapSet = null; + + var selectedSetBefore = selectedBeatmapSet; + var selectedBetmapBefore = selectedBeatmap; + CarouselRoot newRoot = new CarouselRoot(this); if (beatmapsSplitOut) @@ -148,14 +154,12 @@ namespace osu.Game.Screens.Select else { var carouselBeatmapSets = originalBeatmapSetsDetached.Select(createCarouselSet).OfType(); + newRoot.AddItems(carouselBeatmapSets); } root = newRoot; - if (selectedBeatmapSet != null && !originalBeatmapSetsDetached.Contains(selectedBeatmapSet.BeatmapSet)) - selectedBeatmapSet = null; - Scroll.Clear(false); itemsCache.Invalidate(); ScrollToSelected(); @@ -164,6 +168,15 @@ namespace osu.Game.Screens.Select if (loadedTestBeatmaps) signalBeatmapsLoaded(); + + // Restore selection + if (selectedBetmapBefore != null && selectedSetBefore != null && newRoot.BeatmapSetsByID.TryGetValue(selectedSetBefore.BeatmapSet.ID, out var newSelectionCandidates)) + { + CarouselBeatmap? found = newSelectionCandidates.SelectMany(s => s.Beatmaps).SingleOrDefault(b => b.BeatmapInfo.ID == selectedBetmapBefore.BeatmapInfo.ID); + + if (found != null) + found.State.Value = CarouselItemState.Selected; + } } private readonly List visibleItems = new List(); From c9f611a713916ea36f9a98eb93de4c02c11cbfc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Aug 2023 14:30:55 +0200 Subject: [PATCH 1157/2100] Enable NRT in `TestSceneObjectOrderedHitPolicy` --- .../TestSceneObjectOrderedHitPolicy.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index ee70441688..8c2d8ea84e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -380,7 +378,7 @@ namespace osu.Game.Rulesets.Osu.Tests () => judgementResults.Single(r => r.HitObject == hitObject).Type, () => Is.EqualTo(result)); } - private void addJudgementAssert(string name, Func hitObject, HitResult result) + private void addJudgementAssert(string name, Func hitObject, HitResult result) { AddAssert($"{name} judgement is {result}", () => judgementResults.Single(r => r.HitObject == hitObject()).Type == result); @@ -392,8 +390,8 @@ namespace osu.Game.Rulesets.Osu.Tests () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100)); } - private ScoreAccessibleReplayPlayer currentPlayer; - private List judgementResults; + private ScoreAccessibleReplayPlayer currentPlayer = null!; + private List judgementResults = null!; private void performTest(List hitObjects, List frames) { From ab4d47b594dd45358110db8b68a76705e1089b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Aug 2023 14:37:58 +0200 Subject: [PATCH 1158/2100] Rewrite assertions to use nunit constraints --- .../TestSceneObjectOrderedHitPolicy.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index 8c2d8ea84e..9ee55d95a5 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -381,13 +381,13 @@ namespace osu.Game.Rulesets.Osu.Tests private void addJudgementAssert(string name, Func hitObject, HitResult result) { AddAssert($"{name} judgement is {result}", - () => judgementResults.Single(r => r.HitObject == hitObject()).Type == result); + () => judgementResults.Single(r => r.HitObject == hitObject()).Type, () => Is.EqualTo(result)); } private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}", - () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100)); + () => judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, () => Is.EqualTo(offset).Within(100)); } private ScoreAccessibleReplayPlayer currentPlayer = null!; From 9fd59b807f23c27f298d146c25f737305d73ecfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Aug 2023 15:06:06 +0200 Subject: [PATCH 1159/2100] Rewrite `TestSceneObjectOrderedHitPolicy` to not rely on custom hitwindows --- .../TestSceneObjectOrderedHitPolicy.cs | 117 +++++++----------- 1 file changed, 47 insertions(+), 70 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index 9ee55d95a5..1dd9116da2 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -7,7 +7,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Screens; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Replays; @@ -17,6 +16,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -28,8 +28,13 @@ namespace osu.Game.Rulesets.Osu.Tests { public partial class TestSceneObjectOrderedHitPolicy : RateAdjustedBeatmapTestScene { - private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss - private const double late_miss_window = 500; // time after +500 is considered a miss + private readonly OsuHitWindows referenceHitWindows; + + public TestSceneObjectOrderedHitPolicy() + { + referenceHitWindows = new OsuHitWindows(); + referenceHitWindows.SetDifficulty(0); + } /// /// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged. @@ -44,12 +49,12 @@ namespace osu.Game.Rulesets.Osu.Tests var hitObjects = new List { - new TestHitCircle + new HitCircle { StartTime = time_first_circle, Position = positionFirstCircle }, - new TestHitCircle + new HitCircle { StartTime = time_second_circle, Position = positionSecondCircle @@ -63,7 +68,8 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert(hitObjects[0], HitResult.Miss); addJudgementAssert(hitObjects[1], HitResult.Miss); - addJudgementOffsetAssert(hitObjects[0], late_miss_window); + // note lock prevented the object from being hit, so the judgement offset should be very late. + addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh)); } /// @@ -79,12 +85,12 @@ namespace osu.Game.Rulesets.Osu.Tests var hitObjects = new List { - new TestHitCircle + new HitCircle { StartTime = time_first_circle, Position = positionFirstCircle }, - new TestHitCircle + new HitCircle { StartTime = time_second_circle, Position = positionSecondCircle @@ -98,7 +104,8 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert(hitObjects[0], HitResult.Miss); addJudgementAssert(hitObjects[1], HitResult.Miss); - addJudgementOffsetAssert(hitObjects[0], late_miss_window); + // note lock prevented the object from being hit, so the judgement offset should be very late. + addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh)); } /// @@ -114,12 +121,12 @@ namespace osu.Game.Rulesets.Osu.Tests var hitObjects = new List { - new TestHitCircle + new HitCircle { StartTime = time_first_circle, Position = positionFirstCircle }, - new TestHitCircle + new HitCircle { StartTime = time_second_circle, Position = positionSecondCircle @@ -133,7 +140,8 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert(hitObjects[0], HitResult.Miss); addJudgementAssert(hitObjects[1], HitResult.Miss); - addJudgementOffsetAssert(hitObjects[0], late_miss_window); + // note lock prevented the object from being hit, so the judgement offset should be very late. + addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh)); } /// @@ -149,12 +157,12 @@ namespace osu.Game.Rulesets.Osu.Tests var hitObjects = new List { - new TestHitCircle + new HitCircle { StartTime = time_first_circle, Position = positionFirstCircle }, - new TestHitCircle + new HitCircle { StartTime = time_second_circle, Position = positionSecondCircle @@ -167,8 +175,8 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } }); - addJudgementAssert(hitObjects[0], HitResult.Great); - addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert(hitObjects[0], HitResult.Meh); + addJudgementAssert(hitObjects[1], HitResult.Meh); addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100 } @@ -186,12 +194,12 @@ namespace osu.Game.Rulesets.Osu.Tests var hitObjects = new List { - new TestHitCircle + new HitCircle { StartTime = time_first_circle, Position = positionFirstCircle }, - new TestHitCircle + new HitCircle { StartTime = time_second_circle, Position = positionSecondCircle @@ -204,8 +212,8 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } }); - addJudgementAssert(hitObjects[0], HitResult.Great); - addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert(hitObjects[0], HitResult.Meh); + addJudgementAssert(hitObjects[1], HitResult.Ok); addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time } @@ -223,19 +231,19 @@ namespace osu.Game.Rulesets.Osu.Tests var hitObjects = new List { - new TestHitCircle + new HitCircle { StartTime = time_circle, Position = positionCircle }, - new TestSlider + new Slider { StartTime = time_slider, Position = positionSlider, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, - new Vector2(25, 0), + new Vector2(50, 0), }) } }; @@ -265,19 +273,19 @@ namespace osu.Game.Rulesets.Osu.Tests var hitObjects = new List { - new TestHitCircle + new HitCircle { StartTime = time_circle, Position = positionCircle }, - new TestSlider + new Slider { StartTime = time_slider, Position = positionSlider, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, - new Vector2(25, 0), + new Vector2(50, 0), }) } }; @@ -285,11 +293,11 @@ namespace osu.Game.Rulesets.Osu.Tests performTest(hitObjects, new List { new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_circle + late_miss_window - 100, Position = positionCircle, Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_circle + late_miss_window - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_circle + referenceHitWindows.WindowFor(HitResult.Meh) - 100, Position = positionCircle, Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_circle + referenceHitWindows.WindowFor(HitResult.Meh) - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } }, }); - addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[0], HitResult.Ok); addJudgementAssert(hitObjects[1], HitResult.Great); addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit); addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); @@ -302,7 +310,7 @@ namespace osu.Game.Rulesets.Osu.Tests public void TestHitCircleBeforeSpinner() { const double time_spinner = 1500; - const double time_circle = 1800; + const double time_circle = 1600; Vector2 positionCircle = Vector2.Zero; var hitObjects = new List @@ -313,7 +321,7 @@ namespace osu.Game.Rulesets.Osu.Tests Position = new Vector2(256, 192), EndTime = time_spinner + 1000, }, - new TestHitCircle + new HitCircle { StartTime = time_circle, Position = positionCircle @@ -331,7 +339,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); addJudgementAssert(hitObjects[0], HitResult.Great); - addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Meh); } [Test] @@ -344,12 +352,12 @@ namespace osu.Game.Rulesets.Osu.Tests var hitObjects = new List { - new TestHitCircle + new HitCircle { StartTime = time_circle, Position = positionCircle }, - new TestSlider + new Slider { StartTime = time_slider, Position = positionSlider, @@ -400,7 +408,11 @@ namespace osu.Game.Rulesets.Osu.Tests Beatmap.Value = CreateWorkingBeatmap(new Beatmap { HitObjects = hitObjects, - Difficulty = new BeatmapDifficulty { SliderTickRate = 3 }, + Difficulty = new BeatmapDifficulty + { + OverallDifficulty = 0, + SliderTickRate = 3 + }, BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo @@ -428,28 +440,6 @@ namespace osu.Game.Rulesets.Osu.Tests AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); } - private class TestHitCircle : HitCircle - { - protected override HitWindows CreateHitWindows() => new TestHitWindows(); - } - - private class TestSlider : Slider - { - public TestSlider() - { - SliderVelocity = 0.1f; - - DefaultsApplied += _ => - { - HeadCircle.HitWindows = new TestHitWindows(); - TailCircle.HitWindows = new TestHitWindows(); - - HeadCircle.HitWindows.SetDifficulty(0); - TailCircle.HitWindows.SetDifficulty(0); - }; - } - } - private class TestSpinner : Spinner { protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) @@ -459,19 +449,6 @@ namespace osu.Game.Rulesets.Osu.Tests } } - private class TestHitWindows : HitWindows - { - private static readonly DifficultyRange[] ranges = - { - new DifficultyRange(HitResult.Great, 500, 500, 500), - new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window), - }; - - public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss; - - protected override DifficultyRange[] GetRanges() => ranges; - } - private partial class ScoreAccessibleReplayPlayer : ReplayPlayer { public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; From a1b4a5621592234f4787d26a88d1b03830661c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Aug 2023 18:04:20 +0200 Subject: [PATCH 1160/2100] Add capability to export ordered object policy test cases for stable crosscheck --- .../TestSceneObjectOrderedHitPolicy.cs | 86 ++++++++++++++++++- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index 1dd9116da2..5205400a7e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -3,12 +3,17 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; using NUnit.Framework; +using osu.Framework.Extensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Formats; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -20,6 +25,7 @@ using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; using osu.Game.Screens.Play; using osu.Game.Tests.Visual; using osuTK; @@ -30,6 +36,13 @@ namespace osu.Game.Rulesets.Osu.Tests { private readonly OsuHitWindows referenceHitWindows; + /// + /// This is provided as a convenience for testing note lock behaviour against osu!stable. + /// Setting this field to a non-null path will cause beatmap files and replays used in all test cases + /// to be exported to disk so that they can be cross-checked against stable. + /// + private readonly string? exportLocation = null; + public TestSceneObjectOrderedHitPolicy() { referenceHitWindows = new OsuHitWindows(); @@ -401,12 +414,21 @@ namespace osu.Game.Rulesets.Osu.Tests private ScoreAccessibleReplayPlayer currentPlayer = null!; private List judgementResults = null!; - private void performTest(List hitObjects, List frames) + private void performTest(List hitObjects, List frames, [CallerMemberName] string testCaseName = "") { - AddStep("load player", () => + IBeatmap playableBeatmap = null!; + Score score = null!; + + AddStep("create beatmap", () => { + var cpi = new ControlPointInfo(); + cpi.Add(0, new TimingControlPoint { BeatLength = 1000 }); Beatmap.Value = CreateWorkingBeatmap(new Beatmap { + Metadata = + { + Title = testCaseName + }, HitObjects = hitObjects, Difficulty = new BeatmapDifficulty { @@ -417,11 +439,69 @@ namespace osu.Game.Rulesets.Osu.Tests { Ruleset = new OsuRuleset().RulesetInfo }, + ControlPointInfo = cpi + }); + playableBeatmap = Beatmap.Value.GetPlayableBeatmap(new OsuRuleset().RulesetInfo); + }); + + AddStep("create score", () => + { + score = new Score + { + Replay = new Replay + { + Frames = new List + { + // required for correct playback in stable + new OsuReplayFrame(0, new Vector2(256, -500)), + new OsuReplayFrame(0, new Vector2(256, -500)) + }.Concat(frames).ToList() + }, + ScoreInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + BeatmapInfo = playableBeatmap.BeatmapInfo + } + }; + }); + + if (exportLocation != null) + { + AddStep("create beatmap", () => + { + var beatmapEncoder = new LegacyBeatmapEncoder(playableBeatmap, null); + + using (var stream = File.Open(Path.Combine(exportLocation, $"{testCaseName}.osu"), FileMode.Create)) + { + var memoryStream = new MemoryStream(); + using (var writer = new StreamWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) + beatmapEncoder.Encode(writer); + + memoryStream.Seek(0, SeekOrigin.Begin); + memoryStream.CopyTo(stream); + memoryStream.Seek(0, SeekOrigin.Begin); + playableBeatmap.BeatmapInfo.MD5Hash = memoryStream.ComputeMD5Hash(); + } }); + AddStep("export score", () => + { + var scoreToEncode = score.DeepClone(); + scoreToEncode.Replay.Frames = scoreToEncode.Replay.Frames.Cast() + .Select(frame => new OsuReplayFrame(frame.Time + LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET, frame.Position, frame.Actions.ToArray())) + .ToList(); + + using var stream = File.Open(Path.Combine(exportLocation, $"{testCaseName}.osr"), FileMode.Create); + var encoder = new LegacyScoreEncoder(scoreToEncode, playableBeatmap); + encoder.Encode(stream); + }); + } + + AddStep("load player", () => + { SelectedMods.Value = new[] { new OsuModClassic() }; - var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + var p = new ScoreAccessibleReplayPlayer(score); p.OnLoadComplete += _ => { From 64786aaee8d99de8757648635cf1daa3d7ec75d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Aug 2023 10:43:48 +0200 Subject: [PATCH 1161/2100] Adjust test cases slightly to avoid running into hitwindow edge issue Some note lock test cases do not play out correctly when exported out to stable due to a completely separate issue, namely #11311. Adjust the test cases for now to isolate failure vectors. --- .../TestSceneObjectOrderedHitPolicy.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index 5205400a7e..fd8973c375 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -184,14 +184,14 @@ namespace osu.Game.Rulesets.Osu.Tests performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } + new OsuReplayFrame { Time = time_first_circle - 190, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_circle - 90, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } }); addJudgementAssert(hitObjects[0], HitResult.Meh); addJudgementAssert(hitObjects[1], HitResult.Meh); - addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 - addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100 + addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190 + addJudgementOffsetAssert(hitObjects[0], -90); // time_second_circle - first_circle_time - 90 } /// @@ -221,13 +221,13 @@ namespace osu.Game.Rulesets.Osu.Tests performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_circle - 190, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } }); addJudgementAssert(hitObjects[0], HitResult.Meh); addJudgementAssert(hitObjects[1], HitResult.Ok); - addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 + addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190 addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time } @@ -343,7 +343,7 @@ namespace osu.Game.Rulesets.Osu.Tests performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_spinner - 90, Position = positionCircle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, From 91c2cadb4737839e36d087c02658b4872cd2456a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Aug 2023 19:13:32 +0900 Subject: [PATCH 1162/2100] Add missing colon in mod settings tooltip --- osu.Game/Rulesets/Mods/Mod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index f9812d6c00..6d40826afe 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Mods var bindable = (IBindable)property.GetValue(this)!; if (!bindable.IsDefault) - tooltipTexts.Add($"{attr.Label} {bindable}"); + tooltipTexts.Add($"{attr.Label}: {bindable}"); } return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s))); From 5555f73e97f22f9c6f0425bc4c9459bef88f3be5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Aug 2023 19:38:18 +0900 Subject: [PATCH 1163/2100] Update test to match new behaviour --- .../SongSelect/TestSceneBeatmapCarousel.cs | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 61f95dc628..b0aff8b4db 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -758,7 +758,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestSortingWithFiltered() + public void TestSortingWithDifficultyFiltered() { List sets = new List(); @@ -777,13 +777,32 @@ namespace osu.Game.Tests.Visual.SongSelect loadBeatmaps(sets); + AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false)); + + checkVisibleItemCount(false, 9); + checkVisibleItemCount(true, 1); + AddStep("Filter to normal", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Normal" }, false)); - AddAssert("Check first set at end", () => carousel.BeatmapSets.First().Equals(sets.Last())); - AddAssert("Check last set at start", () => carousel.BeatmapSets.Last().Equals(sets.First())); + checkVisibleItemCount(false, 3); + checkVisibleItemCount(true, 1); + + AddUntilStep("Check all visible sets have one normal", () => + { + return carousel.Items.OfType() + .Where(p => p.IsPresent) + .Count(p => ((CarouselBeatmapSet)p.Item)!.Beatmaps.Single().BeatmapInfo.DifficultyName.StartsWith("Normal", StringComparison.Ordinal)) == 3; + }); AddStep("Filter to insane", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Insane" }, false)); - AddAssert("Check first set at start", () => carousel.BeatmapSets.First().Equals(sets.First())); - AddAssert("Check last set at end", () => carousel.BeatmapSets.Last().Equals(sets.Last())); + checkVisibleItemCount(false, 3); + checkVisibleItemCount(true, 1); + + AddUntilStep("Check all visible sets have one insane", () => + { + return carousel.Items.OfType() + .Where(p => p.IsPresent) + .Count(p => ((CarouselBeatmapSet)p.Item)!.Beatmaps.Single().BeatmapInfo.DifficultyName.StartsWith("Insane", StringComparison.Ordinal)) == 3; + }); } [Test] From a64381f8553dd49b9c5c5142aa2934c333b101a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Aug 2023 19:43:08 +0900 Subject: [PATCH 1164/2100] Add test coverage of add/remove when difficulties are split out --- .../SongSelect/TestSceneBeatmapCarousel.cs | 47 +++++++++++++++---- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index b0aff8b4db..5af6d862b2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -39,6 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelect private BeatmapInfo currentSelection => carousel.SelectedBeatmapInfo; private const int set_count = 5; + private const int diff_count = 3; [BackgroundDependencyLoader] private void load(RulesetStore rulesets) @@ -501,6 +502,36 @@ namespace osu.Game.Tests.Visual.SongSelect waitForSelection(set_count); } + [Test] + public void TestAddRemoveDifficultySort() + { + loadBeatmaps(); + + AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false)); + + checkVisibleItemCount(false, set_count * diff_count); + + var firstAdded = TestResources.CreateTestBeatmapSetInfo(diff_count); + var secondAdded = TestResources.CreateTestBeatmapSetInfo(diff_count); + + AddStep("Add new set", () => carousel.UpdateBeatmapSet(firstAdded)); + AddStep("Add new set", () => carousel.UpdateBeatmapSet(secondAdded)); + + checkVisibleItemCount(false, (set_count + 2) * diff_count); + + AddStep("Remove set", () => carousel.RemoveBeatmapSet(firstAdded)); + + checkVisibleItemCount(false, (set_count + 1) * diff_count); + + setSelected(set_count + 1, 1); + + AddStep("Remove set", () => carousel.RemoveBeatmapSet(secondAdded)); + + checkVisibleItemCount(false, (set_count) * diff_count); + + waitForSelection(set_count); + } + [Test] public void TestSelectionEnteringFromEmptyRuleset() { @@ -662,7 +693,7 @@ namespace osu.Game.Tests.Visual.SongSelect for (int i = 0; i < 3; i++) { - var set = TestResources.CreateTestBeatmapSetInfo(3); + var set = TestResources.CreateTestBeatmapSetInfo(diff_count); // only need to set the first as they are a shared reference. var beatmap = set.Beatmaps.First(); @@ -709,7 +740,7 @@ namespace osu.Game.Tests.Visual.SongSelect for (int i = 0; i < 3; i++) { - var set = TestResources.CreateTestBeatmapSetInfo(3); + var set = TestResources.CreateTestBeatmapSetInfo(diff_count); // only need to set the first as they are a shared reference. var beatmap = set.Beatmaps.First(); @@ -768,7 +799,7 @@ namespace osu.Game.Tests.Visual.SongSelect for (int i = 0; i < 3; i++) { - var set = TestResources.CreateTestBeatmapSetInfo(3); + var set = TestResources.CreateTestBeatmapSetInfo(diff_count); set.Beatmaps[0].StarRating = 3 - i; set.Beatmaps[2].StarRating = 6 + i; sets.Add(set); @@ -857,7 +888,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("create hidden set", () => { - hidingSet = TestResources.CreateTestBeatmapSetInfo(3); + hidingSet = TestResources.CreateTestBeatmapSetInfo(diff_count); hidingSet.Beatmaps[1].Hidden = true; hiddenList.Clear(); @@ -904,7 +935,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("add mixed ruleset beatmapset", () => { - testMixed = TestResources.CreateTestBeatmapSetInfo(3); + testMixed = TestResources.CreateTestBeatmapSetInfo(diff_count); for (int i = 0; i <= 2; i++) { @@ -926,7 +957,7 @@ namespace osu.Game.Tests.Visual.SongSelect BeatmapSetInfo testSingle = null; AddStep("add single ruleset beatmapset", () => { - testSingle = TestResources.CreateTestBeatmapSetInfo(3); + testSingle = TestResources.CreateTestBeatmapSetInfo(diff_count); testSingle.Beatmaps.ForEach(b => { b.Ruleset = rulesets.AvailableRulesets.ElementAt(1); @@ -949,7 +980,7 @@ namespace osu.Game.Tests.Visual.SongSelect manySets.Clear(); for (int i = 1; i <= 50; i++) - manySets.Add(TestResources.CreateTestBeatmapSetInfo(3)); + manySets.Add(TestResources.CreateTestBeatmapSetInfo(diff_count)); }); loadBeatmaps(manySets); @@ -1113,7 +1144,7 @@ namespace osu.Game.Tests.Visual.SongSelect { beatmapSets.Add(randomDifficulties ? TestResources.CreateTestBeatmapSetInfo() - : TestResources.CreateTestBeatmapSetInfo(3)); + : TestResources.CreateTestBeatmapSetInfo(diff_count)); } } From 5eac604f8b2cf59a5346956d99a7fea0fdda0cd9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Aug 2023 19:44:39 +0900 Subject: [PATCH 1165/2100] Add coverage of selection retention when difficulties are split out --- .../SongSelect/TestSceneBeatmapCarousel.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 5af6d862b2..daa8c9c4c2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -1005,6 +1005,43 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1); } + [Test] + public void TestCarouselRemembersSelectionDifficultySort() + { + List manySets = new List(); + + AddStep("Populuate beatmap sets", () => + { + manySets.Clear(); + + for (int i = 1; i <= 50; i++) + manySets.Add(TestResources.CreateTestBeatmapSetInfo(diff_count)); + }); + + loadBeatmaps(manySets); + + AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false)); + + advanceSelection(direction: 1, diff: false); + + for (int i = 0; i < 5; i++) + { + AddStep("Toggle non-matching filter", () => + { + carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false); + }); + + AddStep("Restore no filter", () => + { + carousel.Filter(new FilterCriteria(), false); + eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID); + }); + } + + // always returns to same selection as long as it's available. + AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1); + } + [Test] public void TestFilteringByUserStarDifficulty() { From d6aded3ac31b232884f0642bedc793f41b377b2e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Aug 2023 20:11:55 +0900 Subject: [PATCH 1166/2100] Update framework --- 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 8ebfde8047..2d15bce85a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + 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 literal 4677 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 Hu0mjfgVw2; diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-apple.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-apple.png index 17f3be9c262953cac52da794d601bb853d8dc276..6a862572faca191c265bd1bdd76283c08fdab790 100755 GIT binary patch literal 26685 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 B13Lf! literal 4677 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 Hu0mjf9r&r4 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-bananas-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-bananas-overlay.png index 2c94ea78bfdba901614472a3215401d7b0a2dd71..9d1d6d1177b9e36bcef557251ba3914fa5acbae2 100755 GIT binary patch literal 23898 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 literal 4949 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 Hu0mjfi%6-N diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-drop.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-drop.png index f259684055ad3ce25b2c3cdb5c3a53062b60a8e9..b3d7e39dcdec42cc39c96f004690d7f6a7337be3 100755 GIT binary patch literal 26684 zcmeFXWl$vDvM$^>jk~+MH|{pLy9_k$u7fkUyEC{m3@(GaySw`gJ_8JPdEdRyIeW)< z|D1^L{@WGNQB`YYKAHJsu2o$X8L6W50U3b+0RR9X%gRWq0RRy1mkMM|&UO5fK3nM%rVN$L;{KT#%S_3cSy`1x4m&6zvh%7g*blEd6>5N z@$!S^{>#JlcqeJVpRUK}xm&&~KEXHD+b4C>ZBG%yq3=`Lr{Rc;kFUcIk0N@$!cTw7 z5IY^Q8Y!l6&pTCoSh1dONiRA@102SWw8rCG*nZ!7`)yeHy%PArAAWqgr3iTSWYTe@ z?!xcb?u8j2M%#jcenF;j>$1A1jNU8Sw|$u#zNvx!>}y^&Z4`@5%nb5>EZn|X8rD9V z-imtllYK}|ygfc>|zjwX+MH%N`-}U;X;jHa`!tgGG-QC~u zY+vg_yTjoM=hL~3j$U(UccgsEQ~Py0#vdf*rye6wASkh&&8N_62nr_L3BJSy0o0>j-;Uj_WAo z;73S!UA57Y6g|CD(1MQoWYdD4{?Ex4fK_RlzI9X6lECF^o6B*T0PBh01G+TPXpG=k zc~Pt|Fc04{%R5ciGTZMF8|R_!hM}sa`Hx4o%9?XouIH+I%O+}pM=;-Yak4Vsuf}5mOl=j>38TJlb0pImt*BEnm-=B=KY+}-5(S`c30 zCRrxR3hOJ8^nZ31$cuZr%Ds;NaM%`b$fLfe?0@_A^*F}hUUB;6tmRurvqsp_ua)Fb zj!I-R(zkBg`=+Xumz&iDd@u^2qE*QX*>3dP4kccQ&)i<=KAFFV(Yk4TrYlh!bJvR5 z@CsjXVo#AywJp8YF*`e2iz(FhIqWZsk5vxXC3_V~REKh$r4z)wTXZBMY$qu0gJ5wy z7Wk@7J7E&-Cld28=N}B5Vg5rlle;YG;fA~-;X`T`+n}RW`6))bzTPY5bf-2?YymIB z#YJyes=;u3+Nm1bA0BdN_QAuc`L@D?7N!V(+&F`Z)O%JvcB0-nEy;N`;iF#_s}DVn zjmpw6p6Y$<)1viXZfX|HeQa0b4eAW8=Udgn^s%RQZYn{Q@aX3Hi2dp;#Oq?qKL|_l znW`ElNQ6*Es>$Hmr!wdX6SmXQ{R)xLxi<*-60^O=nIwG#=1pspVcA=T(lCk;tF_-NZZ>0xi2KE=SK>!QN4;4v$4f$;mQo8U z4El%7k9bE@l-mI&^=d%k;4%}U%OZQFUM)=Pd#vB*_Et(Rw%Kl4{j_F$+$-xQoozgk z^&ynh_gXh`;n0M=xmG@cfFVI!Mt(N3d!itL1~wH^O3VeO2DHl6p&xs-mPD6L9_vs* z5Y%r+v|7>9KkU=w2F>Hy&%y_5UE5X_)o2&mUD|#%)?9LeM(7%BQ7K9X;htEu3u6~e z4>z4@vm$+6+kaZuT))OMZk}IlU8X*qeYEWdnvDs|joV64==jx0GOXZ??l4>h<4QXv zBWRhAe}b3)#tUfAG3r4&@EzE=a?6q!HP?eRb?m$Tb`&mr0ZDe@-R#LUku3N6Af%-X ztZAz?H0UhZ1uQGsd|k~!f3&BcNLDL1&V&+D)4wtIoPr=U?6SkA+vy~)hFLmy9*;W= zrtC!sgs>w|(v!OI757RS()W_W_&%J@6EgY(DbS&O)`@T@nUnDVm#NfDnI(`Icvm-s zQJi!UR}L$pe+!tqvm{*()65qq*P+B;DhR>Z>BK!avp_Fxo*)_t{0viv$htM$To7{t zVr7dVqW&lb+(|J1vZoaIEXK<3+h3>h8BssQnH$k=y}71Zt`fFsI7^;7m2y>;-MuJG z^(^Axd_lHjV%`}{AGTh0h7R#!C#J{}%de-e6r2V9RHA~w6k)%Y7E!RKiV?NWfdb20 z?X(BF05}uT+J%V2}<^Ka(kxe1QMa*JS$IR2ZgJe8RR)w)E9Ccwu zSXPt0W6(%u;H)K~px6J!&i@tmG}YCX`Y!P|Qy``!WKr|s1EQi;a9G%ZxVdVE#)h2B ze4lg(5;PAC6|6(FS*e^N8hkN1y)ja7X&NP-Jj|Ne`cAzIT;c+l<)LAbQHmXO18$*1 z3VdPZC8rKx?dBn1i*rsX2MmuypFiq?xt$WR;8TR*q`5M{JAtFAa^&B}hSIW=q*Y9I zXY&;a|3EZ;KqcPO|8jsXw##4SBi=W>fK*XV3j5V^h^W;|r3A~G67tT+)e%J-Uh!em z9VBP71M=p}KujIHOhyJI(UBwmc5=Tt3K(@{x8HO4RZ{34?I+eqwzX}d?C_xfXT&R2 ze1D+ipjHm;hrXc3`e;&%7eI;^`ZXXryo;s9D^RzAm^#F>EofE4a+9G4@nb(CI|VSW zw8un_O|^Y9Lp>z0@yc*&&!(di!et?e{gzJ}Lys52H@8!6;Cn6?dE^N7@u!-=`7^IP zy6je0HJzIwP}Ttm2(m_SNif1J?YI~kq#;_~87^Lk9{>S)TJVbSUf z5t$*0Dj(3w!6U{x@(fNM3?9_pC=p~r6l&1Kp@GKs<2 zP(_^vYe!3=z7SN)j)EZIc`lX~wat;^wo-^eP)-)62@w{Z1OxqtDyR872>tg5SKu&JSLSO7nyL3 zlO9UYD0S7tB&{K5uFK^@Y-M&{wUjs>bf77r+^sHK3``T`Oref*SQCGvNujVwp3>_9 zD5U(#6lfM$Osc?aVrrWP$;oO6k3658hH6O0Ovi+3*xLLhz1|D1saun^MkpI=P=cj= zh6cUx3gJ!<(l@MJM{PBl0Vde~)ronVP>l;Jk;)44@;6>5lfU9GdN8Ha68X&>uD4`r zQ{}v$cn0j&7_b>;eexFs>1*hUA#Fmseb2`ciU_z&6v9ZAriGNn5!4XzpmHWEX-VVW6^6FPAIcGcGxjH%i2kpnXCZUYt%7UCDfx>W$WWsNjhT$u?j zHiWN2ZK>XD<48M2<3JR#=Pm@0_rh(@E?t&*rlBw|P_8Sgdajo)jL;saE^0ZJ%vnn% zs7pT!A(;=&EbrA%ElZs5Vx~sw6gx&W!VVw!?HY~k>#Iq>m)Sf@sr=~$K|TM}o}P^I zb3uHM2&e&xn#_B864ViLRj8-7ThR~~=){m$RaY3%yz^`4N`-2wuFdu2DGDwvFm zNP|tw4V_G(;)ik|3#{oSspAR^c5_}Ra#A@BbK);7b*S7^i-EO)*v82T+dyFt&@jlWI3b>;aOSRJWWsii~D~f^4#P1gb zkyO7M+uMkELN63%tF@jworb6YVkZQg>8*+ZTiOl~^8I5u{6-<)O75KNh$CH+8$aZq zybc{pwo(`n@HJP%B}OW7LKn)?qwO(cL=NibBSJ#@kO_#66%GaJW17tMX^LGfn(kID zi(p={M-#!|>Wh7lZ-uYAit3G}oC%b`L=7C)VkTtRBnRdM|i zc=n=uj0-OL<>Uxvl7}_45lNVL*Npp5hE-|Z=Oh~ z3+-ub;vzUCNem{EP*qP4Tswfif_fIIwb9h8dXm1j3OdlF2C}>cm9nvmJ#zz$Lp4-6 zMDZQ~t?!w)Lr*FQ4jNH)x0CmTj7uta9yzU))<&r<57W@W8u1k%&oBQD!6qWfc7YNI z)nPp#=p6O|sNROyleZux>P{3TNY{YIz>#qk(O6u`*JQ^ZP{bdAVYr~%xOb5&^ZDyO z;?oArb)b>@1C}?83E#vAPR{4GiI^%0Mp0_DSTz+DY6HSu$tGB$ofjrtJIDimm38Ot zoP8)g0{+VeMWuXDSu zZc#J}{RK2KUG-HSq~$VB%B+<#sp*VtkUX%i>?{?Po3SH)>lWpeEc{Dhk}@1m&$6X__M*&m zfXIE*p0^knMur}6EhWXU$xceFjZS6{DN-vlq4E#zI|!zN1UClvl)Ps2V{Di2co;Vz_7tSw zbtR)ROGoS7L2xE3Tj!{g^C<@ZuY?YS&r)*a=31LhCQY~)L*&%EdQ={H7x^%elnWaD zwHU}S37Q%qG%O~wNFh9b`b7vuvGV-$-8Kx#?8Aez5-7Dt<L)oJw5gPjlRh7&03;w4ac)pr~& z6uQJ&7&Iar%%D1^RnVOu#X2Pcp3na>VwLFeOr=Fv^j-hpP$ z3DS!ch%oJ~M9(~j)HpDfF>?$?B}=+7^00b)uzNj)G4lI}kJwx34F`9BAr6>V!TxGW zt|mq1XgMURPgqq&Aumm}^15t)Ilr|XqNN|yp1Ho?+fyH}Cv z%8AEHyclb*oQp98*SNe;e&69HmIIk)YwZ#nPG_!}hi#K@X~cC~`(M zXW>`0mzUW$rYF`_aDsrVs^tGqD&8w<)#t%()r1##Cu^)!&=t`^qI@ut&RZQb`%xZxE0~pG^{zH=XU92KFA>mmwLE zX2X>$Kf|aGpm1z4r)HVCQ`u#YPlY6MPO)uOvW+ zvPTyK4>DRBdzz7xTcjJmnO!n`hZSkTx|KDzvB0u&9O4$Oa{CdAqiu_9E+{Ch{Ch>= znmir69=k)AQnMnDGNST}4$z3ANJX_^N{KPzbapd4h5IMoH+kv34dQTJ_#sX|X>AO4uF z2ff*%VaBG5(*yJgu@prd%Py;{FBm@k*&u__a+El)u-YL8Jy%$@ZBqC%u0_5(?Q(vakp5=Mwy@p?q_qk5TBS{W!nRQn&=g&L3wSn61~Ry!guLAaq459TCH)=!cRJ&a9!3g(@H# zLy<{OB?UtVHnQJDk6-^;t~5YrK`X!^+Ob*bq7@#v3`ieJtfgE7a`UEhF;8q#OjZB3 zfEx{p2o-&7s$&H%oKPap9Z z!8Zx@6?w?dj?IZ>P3mU;%I7>tHr8IHw9n;^Wv~q2Z z#G*%+`f`q(EQo2{$0O* zO7M9bXvj57T4ocFy91RA<*Q{H)nhDX$K-;Gzh+8Ri%8&%A8@>|Fj~E5gl|~lr(bp| zun{e;1$VNMHchN_H%G0W9JqJbv4;IH&z3B|R30fA(N53+t>0$fO~gem)Wq!uxTGa7 z2Er@qS6OFbq7zNeH*(pC4%JBdd{5(ZIqk@+F?T#~mSk&ePJ|FHpXm_#+!FBIjI_b+ zY4P|If8+If_~44hl!1$52e+*)j*gfxQFwD*KskKd^Nh`}FzD5g+JDGuKaxeqS%|TaxFTP`J*j`; zkPl(3TcOQ>Qm_fo)*om2&?L#e;3Spo&OESsafS42PBbG?SqXNbBY)%Ti$Q+Jt#CIg z;7C7$oy%ACd!G1M!A~(G_mSXoj6MMhv}b{dsuhR=1RI8NXx%R7B>H8$CXjB-@+$FGF$Rk^-s4>6o7*l33 zbgw(BhtEm3>1RbK+PB=b7x4bHTci1QyRlHDqtadoAFAePrGQc%T9T9NOrNIN62Id* zXN^4>HL)q$*xBA$+p`3M6<@M~`xzIc0=&|=KFWl0=C|07j=#KEpa1TL)$D;zIEDZq zzpOZr;Y%RohLVD<3)DTx=cMYH;^xWd>G50=*)&>;7hJ5NhLDy9{8rV+tp-@2$={fD zhGom%vk;gx+AnbZMqu>LN{y}UA8~9@V|J6-T(444H1y$m`l;#RKD$_j#Edr~HJb_d zAWwiB@6^#j1{jM;a9A!Bs4Z9i50G>)=w{%$y#NaPREdD9&R&(O4lfC^2tao{#9Pga*(i?7l|rjd?OKXO#{x2TM7fWc z!8SagUCK#Bv{Bs3U}<2^A|9hbMD>Efmr_^}9`(?|o6~kZuZKCJMOx@4ZwOV$Sna=6zwZyF;_Ck0C&}0+dIzKR7{Bk~?QQvW7E$ zpx~Wb-F@K%oRjOh=fv{---{iz;$=3d6>aVWfq2V6Hvfw=NU@EX7j>MOO{IoO^m>!K zec#>WwKe@GjtxTAJ`#gD5BF>-DVU8l+86@ZbYQ)(Ya@aombx)(b;}yH=K$47c_nQYUw}H3UTQuU8dASJ z;eV6(%zG_6dyu(QAS(($pw~q;FTsGS%H&9~SMw{sFre*LBT(HDltT|6#pym^#gfW@ z6E)MLu#O}Zgohi2YD+?6NqCVW$Uzi>vL1BEO-8=5n=FF)$-U*xC1gP!F8TtO-{9fG zMw<#Z?UmupL+Qnk%kAT)#%8^+8>9z=kY}yzQcTFUM0ER1Wb?ckt4~f8sW@IIlTyi*ND(#PmF2jgT%nYN^Dj$1JjJ zsWfl<Low3UhNbH0hpz0QC&K`($DLM23^z zFwMm5DSM-malV2P!F|&mtAC!;1(+hO2 zm%UXfU+|+J`{`Y`*>651uf+NEd@%F%hH^?S<==x^lf#j&tE#LUV(Z}6_r`7FkIzRA zLll|^9C;mK+4+YP#GYt}ONieFXQfT6m{PL#U`A^Dz9ND8o*H-w%4_z^OSTH6m(w-@ zIc@5(kc-7H>fef&dcuYZ?w$dPh6@$Xff*`ArC|j39SisS({pt$uBeE93STdM;lc*7 zQ+13F;iqRTlxh!~OKH!zG>qMc^jv8Q(z>l*Pth}&_ z+fdR3yzEcJ-OC=PP!g@mRjZ@NOxbSOX|0y4Y=m+TeB-5`i7^Nbq}a-f`dZh^@CzGB zyVHo4W_q6yBeRqdGk#_U+q_K{tHELuXf%z1^w2dZ`~`({lMBz<>Bh;!~U)BBB1mNU5`>J)COj z#iNsA#I{5>KLVmnS2%ZgvWx?1ICoQS_XxbLn+@iu#URx7XB1N*+2qSk zY$DotYjkjje9{I-hbTP~JW(~jBiT4b(F`Sjc(jIw;%s6V^=U3LW5efIN){}}uNO&46a znSrr7iOr@9Bm}QEY>aRsmit69pYQiwxl#IUk8HXv_n(C;tXd^4_mRrwgl2xj0)7$2 zQPP;6L8G$yEu2Q0^uk90o@lnq7nMw#`4hj%s)^s@exrP?ws>v0 zP4yK&0xqhq6}vR#AtNHx4re#rWqIWx}{1`7~(_sms4R9 zhGR4&*VRhVR@s!XMVz2&XoDt64)g4BuCbapw2fE_lXwE(yy2b3{d!e@PwiPb?;!35 zv1Eo+Zay^y(Q5UC6G716AviHt!xzXYkt8eljT;w6d%Uk+iBD5l>7!$Vl>e8a@nEoG&&f;TT(-FNLGHRC$~)Mg< zB>umJgATK?e;4&+i7@=A;EyX|c51R@(Y4y!>*_G;wN5(qo)~p)EkEJPL@`cyl{QC^ z=B3)Dr^`~mlBT&w+r#?VDfS4=x*|~Dl0A-#R1+OP0y>QXf-FrsR>KWk z%s-?|6GW$eJV|BApG5gVNE7O#?ei=>(t*ihSSf<@_oB7c(+tGbC>(MhU7-Us@s&C% zh+=5IPfTLoUnqm?3~P4%5=Qt5qCWU-z(q#r5cf$#A)Q=8=5UqQe2EYH1!48`YbVsn zF#dGz%hd*1!qVHUz&x2*`+V7QLBR0qgm!Ynt#>(MM#~RB1bJ@@>tE27zU~pr68(+a zn~M%J+F@6G#*p5XZ3+ByLP{0%XSaescH91Lkm+3Z{^r68zB7n9W?GDlTDE52KZJ z8`i|FGX11*o4^TH2V0Z#JQEQi>FlCEi47RlVUzLb#{?_>fqGvUr(Xqh9jR0zJUs}X zxmFp@u`z67Wh)gwIAah10ch+k^urMhya=Ma-^@~SKM_3?3P-E$CHcf1NQ+qBk?j&F zqo+Ys3CHm<1v|((T8T|N!vF)aq{!61K!t4k*wmXiI~48s{!!;pcosbIC`b|=T$Mcb zNg)IlI&N9r_Y0hxP&n#@@0Xdyt$w)uTW>MXn3IjpFd<2w1q$_e){|!xXzgM7K%j$| zJ`T+!C~r8V=`8qn==+(x)nv)E&zP>*IB3!Vx$N8ZF>im zSTxDuu}!Lt-Q|+8qGgd}<*t!^G5s=7f_E5O8F`8U%b-_Taij!HF1ulV3vYf z1*%;78MqzcUK1b0en?v5MCJ{&s}kUIxBF))^ph&PW;gM+^YZvDa_m z#c`=0XRth}r3VN4$cbBvS8pkBV)GQLDY1kX2FbKokE~bT3YNqiuqUR=wo;~+jrKoZ z-meYoc3L+bd#gaZ+lraT4`02)pG&NGg)N?kRWDt(fF|6|GXuJ=5x@UbQ6j42U%l2*xGHWv- z3T==go1&A1xs|odCl_<|Pf8l5pKML}%qT>J5d^*X-vR8+-N0mC_I3`g{9Zy7f8+AM zU;j1CNMnS`T@IT;rV7YiG+l$W&!2Zb;KnV^fA1;3i4^gkiq?}R9< z+}xb_Sy??jJy|?CSsYy~S=ssc_*mIESUEVD-zAt`y&c@ZUd#@zlz&0|14Gi>)zrn> z$<5l)f$T3#u!*C)n-B%X`#jly`DgE>sQBOT4zB-X;hhgwFR&9UI}01Dy*=x{YPh;d zdAx)C)1m*XhO5SV>oKdExvQhQi>bMkhq;3r<-bChnf|xFle>%E-{F{vk%*AGA!VEU! z=3q88H{ms9=QiW!`8Nn<7wh+`1l#?qSARj7y+fIqahsTeO*oh>_;|UPxy-?Q%wP^4 zc4jb$hmGCD)Qpdho#$_F%uM;E9bN3f@9DI*2V0u6IyqSWJ@FUe{9-DyLKGYAg=!r(lU2(eJ{no zK-t+?IR63sS6=wvqj_f*{8ypA1N^P=9t^*Pi#gcM(M7}2(N2isFQ3T%()_!=$prtA z6&Y*ScM0#mO8%ckuWs)AkGp@Y0Xyrzr^v|uu3LVv=|7CPf<4U5{xpvrA{cBzSSIB~_{~uBW{}%X{jo@AHA7k$u%=>=D`rjMYKS}$G$Nz`NKa=tQ z(84?Pe~kRE`27!E|Do%D#lZhc_&?S4AG-cm4E(Qz|5IK6ztM&8Kd(II4)1@1Jl|hv zwz3U~-d~0wP2@jF0sxh7zl|&PTk!B zf&~D`0J4%|8eVHZb7dX%SDP*a!i!gUFz6F)z78eX%QjQ<(5F`=RU#zeb^uU4okT=q zIY!3m%4GwQE_P5L7m5>9Kf>ZK!nJ;mhCA3I^R6f+;L&8zcQ}x)|1^q0t!A?A+TF|d zfoi(oP$^0FkqH8pi8v|S=j$({pC9%2bP`3ySF8t>;r!REic+ekrhZ<&6jKcg4CH31p9iB{@N z`x&ZvulM@H;ZPj}Tb*`$f)pREdxL?P7NO}F7Wpy2_k&aG2e0#9X{yKf2y{xNsF;yN zTyK|yktFgDU+O)?FuD)o6&tG5l5N{z;Dq_%F$zC>JU8z3M&{z^)f%h5f=q{EfSfd; zvt(d#NN96tGPD$W^+JV645=tAx`Z;(AV|JBs)#y?6lQK~GbV4IT_?xG7hDjhq*D}_9$y+XjN;au@M58EZ0L4BG&{4mXeKj)(ggrTi z7V~X&+8c-@J}3NW@`cHIu2>eGa)LviseVEoBZZI@xj4+si(0yFfADiWncp2}51{j* zjwC8Jz>p!4c8npKseS+fY(f>0T%xmjA&m)N5d`Xn;mJjEjRJqN9u6nOWvNE4(g+r$ zSEGuon`Cemw1`7aQY%nR6txkZM^b& zJL6MQ5>(V6r8q5^#+0KmRJw?cuw?5-i5e#A`)JTjs75PvKM#ooOLIXe1tYNyS%;kr zMB}3VPPY)vqGI2pl1-$N%0l9EIiOQ1lnI*qh%p?Z3wDW4VshCT2qnaIFo!|W`#48t zCW}Tp1$!%K5+>EBw#EkWWZi{{Y9F-~&n`~dg{xPPpC4k*SAQ^?NG_X$7b)~jP|-sf zprMe!E-GF*=cx<$5d~;<-tP^^$blJEGt zf@PD}mbSHXmtpjYl*M)KCw}+)oRJMpwgP zm`LzES)mNXIQGHc!8qoVBK6-`xnF!{QeDzD{zAPJi9xBDg@o_&>&|e`c1t9%$$#?c z*REt=7!;gR&coHbqKE|2Tx`Jhom(c8Y+?0PhJf!hB#{2CG(nW#q_pa-ZdN)$Sh{V3 zm-g!7oQ+C0fvitmE5kB~x-T}_U{ziZ=F~EQ{LcwRz8mGrZ*Oo6_66r45@X0J`8^E< z)G4wZPR8j0Q?dewLc)??ho4vwJmLiVIK{coV44ASC&N6|{BuY*aj)kCFu*i;ea%R} z_9u4LcYNazFf8x~CR3VBoqPtLgKM^rT}Pzc9=fky?B@I8_z@~r}C|RsvimMHc(I;li`g!$p9=9*=7w`tO6nEP+bC+crvdQ9V-}X(rsU>6% zG+)2w6sJtYlI7YM1wOae2vWQbO&^Q%TvKzGs$BK=xuy_BYk9<(Ve28h62xK7{SKyf zyZRBXWpAg2Rq=6jHp7>A!^YWudK&L^^tCTw(s8qQH)s$Ajl+gRMi=8x35)u+N*5`` zw9HD%*}(fiTf5p`G;+wsB`HXO+{Rg10G$0ql%|K|@A7Ae(+Doo2{-mpY8DQ0v}sN| z(?}-Ujbj$n-&`X4i3QT%zS)kgVt5EPV&vzkPO&RZb5;@rLjwCIZiLhoZd3MVeYMBY z?ZT*-yiQG0JX;6wdM$mwF|jwV?f^a8hE1F4>IP-` zt%u7v{V=p3AFlU%KPSUgNApUNqv)TMP|mH01tSkKATPkg~PjUzCy zSS$1K$mo%NQsQm2d*KfZcSqxFFWNER^a^M0wjldbrg;>rx8(EHEmZi`1*U>_R|h_= zSXkRYc+Bj_=r%#+m>L-!jjHCGa ziWj%UBY~yaK!_9sBhqBw0cgKQaMhDHJ5JlQr=XeU=$SV2rMmz8K+Idp_5wwaf!N6* zQaZjRz-TjeysjT%-nZ9Y3#G-ytLO4r-(UN&PwV;4HO|v3ulGC57Yh1j9h0zi-)V}9 zA^TkZnd*XQ2o{b}JgCO~nthzK(Yl`HjRO{W$`8m&Y~NC)4?za_H6CL7jHfD zRZFD0-8jHO)g_m!{O*0D{Je+9{;K?3*u1(JnkPKv6vT+M2!aX5$`(9;<;k?Tt&K8- zdx#4-I!b^u3y@B z6j{pJ{#fE#NJTA~q*!PsgJv0ll3US;(HBR|C{l`H?8wPmaDEo%sMUI4q zPQRM2j~(b7@NYZEOEr(uwNY$|W9GZ`hzUUD9CBQ8s0{0}B$>qe80=pP($J=&x_ml2ooVQ-jagt0ipA_x|!B&RTTr4rB?8HPgaTEu_#?Dx_G{9Uf zJeu8q=qZOrqm14q-Mi~JZv&jlD>blgd7pQVDx-5XH|{NbK=GL$PA|bS`GfDIQ3km? zTe9p&CE;L9^u56`zx*P$@aLDs*FZEiG0Dhe@dWBJ!~$wN zHdOd>qPC7<|KYk$$?u`Le@uGY>pee$`3M1V zP}wM`jm-1%9C)A6^KJf>D_h}~nkO^WqY~G;wwvVTJ3qsSJfXPouw6;@XvGMfae-tC zg*-)AaMiat)++=^Hbu=mBUF(iDCzUs?g90#Bik<1pb!d{@P!2@-|{&HO0r%isha`s zJP5_nm8d+`o0SK7bB@)!S+YQtYHTI`%pC^YlZDJXtn-aM`7ZN1?tRPJH}A2ga7ZcZ z4_;~0)Kx26K9ju_8B3Q;TAS9gZFpxT>DH*Pn4e-AmFm_o+rtE0q-g_Lw zii$=_K}%uy_9Vc<{0QNdC&DzUd#)@iVE)$1#^^uPI%fXKo$rlHxouF}cig39%CYt> z)dfn5o(6{wf<(b>@kbMoR>*)tIfY0yF#ubtq2swvRJ@VB*T=vNzU3&9gV1*{-FKS9 zTH*;605ivAI;0Rr%AOwyzk!r!n$9sl7v=@?uCx&h9jMETzXN9gmPxOfK(omD_LxTf zt^*GvrzXw>LDP`|+$-NTVw9j7tz(=kmOM3A|B0b7mvF?nYXhhJf^=&DYkPlQrcBu{k=jvGQW@*l1i4`(T^{lHS%uACR)L*%_$BN&DH{TnS8yxH7-a>BwR6aoo?#|~`G_l9 z-=pYoRh;yTb0we$y*Do77*@+b2lN8T0xMfDlN3(j0c63c!5U|b=1?iUg+et9RuMl?7ad=m@_>AU+Cf=XqVwh zAKZjL-~GWmBEb0R17_(g5aR!aQsJdR6R`Wle9DY_XuR$fUb(8|9KO zMqUqipcBoaE^Qn2D+KL3Q2%gqO_aZbr={T|c=&k^?73V(Z9+E-iH@R>Y*JjSS=A;*m!vR_^UEW(NvAU7DuIhMmqtC}d%m zX6~V1%YDmJy%s=HV+@6qjo^)U;*1FI$};OnMG4C^3$GsOh##REIPUwrUy{s?(B1*RKf2VW2#8JKc_s=Ye8d?SLj(}kobSn%+;y7Wq)Y7}Gn zA?-vx(qIRQewBn{ z`sLKsfkiW#5o(hkGVM?1Fw9S)F|xcT;9zGx+mEdMZ_F2Ult9YMw=h-F2;z&`A^4vl z3(>+LKnV-FdPsI|0o_4xfgyvqHih zq}^A$k+LVQ6R6d~?zcr}-^-PRV;FWd3r;YGO|Iq2xaM2+^bJIg8>gg*n4$;mkl#h^ z$IL5J6^R5vS-Y&KA2#*UVz9ijo{F504KT*2YVT^&xKmlQa(1ra0(P=e5>JFkFbHV%6=j4G@IU&4*vXgb+kj@3cL5lS^>vx3h+kk}{4(@T9+yB$Td55#zy?;E3 z7(uPrGezv$BSwi)1TjjL))tDYhSH)+klK6HR#ls}R$Ejn_TE%@Xk+gh?W3j0FVFY- z{d+$5xz2Un=X}mN_xt_2j+X12P?9!jL$s0(B3`j6jAJ7|nJ{|kiGb_{q(1enYKeA3WCZfrt|bSgP;0vuF}jEZ`ek9Y#oAN&}c*Xh1gs}h0{m$iQr-$H2Ng?R4j z8EUqFsY}q$YHi09-&S8{8#i?_rp6CHJf?M@a%*YVa;dNrx`0$6+o zDhZCi21bm=ZK!^i;iG5@oa(3OaF4xEs>ydY>!S_5iittT!0tjZ*q!Ed+*jIqlGp&? zS-xrpA~o1Jki5{SdnKJU?l{e;MyKO8E~{`J077c&ScelbRGD|;TeNmchXE2JLW)9*fjfm#Ugph4|!&DYlS;C z#Ml7(1{KWX$(?G`>z@o^A+2YPU2euT_65K%$;l1>Truvb9;20MGBlVj-}S(=w(Gh7 zP>VwniAs z_se{`Iap6duh3=@J#6P>{lpFA1cGZWKKTH#6DVe2VLlT9+%!^iLS}H!;mf_eGe|q4 zSQ`%>Y4NH|ed|N5)+sm3rM_J*XIEs z0^lN_WkZfj`zHW#kEjA;B)0p=n zVw4>$K^DV&n?0XI35g%t{PA0G?z^@5O<>Se7bH&PgneU;A&kB zH64SW9!GzPAs~yDVB2zFrKT)x+ z;qCIPY)WB?b;+P6BrxkZC12(kH+EL^g>%pNg6aopQ0T-e0p@V~Wi&3CN#L9zK$Ix# zgU6u-=?&+FbQdxJFcM!=<1_>qkGt9v22 z>+pkm<|LD6rqJeP&Nam0luuVG^QQ{dc4R&R;?CaFPy&Lil&LH#yZ0Lr3}P!-9g3;b zM9YT-DLq67(uJ!FcqXD!A47rdyg-QzQS+||$jo{3lbaHz4s*oms2p6p>S$TW093@h z^S>{@p64@2u!vV2QOUG;t4t=`jRip1*po7mqZUHd2uql*UMxWN7C?r#@@n-{V#!8@ zFeHOlt)3nzqag(IHURX?uo>@!0d4@o+C(iQMJ1_#DLf$2AlJ{r0Kp~yLt#mMKuQsy z%=mt=PhVs|9!e>MOfZ28uXRV!|5BP#y7ACYo^}rLGWL|fJOF0UJJ_cyrb!#L7Rbyu zvj-~Ktmfe}XE1k!mza%l(*=!G30jO2CG4@TweBEBFe02TZ8i^}X;<$$ONX472lmf^ zvs6G6AkI7TyWkRJC#7WsycHtl3d~HofJs;>nsoLvQ4R4Ho8-zDq{(~xd;z)ElXKIN zIxDCyJf`cW5nzZ0#IFgQc-@AFBvMOfm;>n9uLzN@9RlyLa4z*8#xNhAz;Mmv{bicZ z)RImk{HAL+p;%g?J*^RJRstZ^5vTGUE|4{mA6Nb zxK#N7EdD~!+p1w38)Hr^Z`Smb9F`=dTicT^S-TKr-lLm_=afV-YFCBUUr z)jAl(P#IGRR}?kt3G64_!SaEydtgFxA{7}7w^^jj34++$bUvi_0^_&Tq>(&P3ZLtt-wE>XCG@9I`hvh}0H zD&0{NHdTj~pb#8L;RD(remmvL2U1UECU-Fs?*^_WIQ z+}?W{9UZIXy|R~ICgaW4hDl2X04eFBo?Z1wXNOE-(4gbJoeI>jnhE8PbO4t)cAJJo z1v9y3z$^@&TrUi(Oj>GDtO)Tw7~<9xXp@c%Fo;jfk-zFJ+2}~Nm0TUl-6;Po?SA4% zSIdffAdFczeI4N2P_B=I>2d%WeSB)f0ms+%)H#43b{wR?>e4<|hq}b+DrIDVGq5up zfORQ#=YctT_U*f*HpCiW3dm(qrSomwoAx+0`M_5&&)C#@C^VMW49^k zUG(eMN>;Mbr~kI`eIF*I)k?mOr`^5e<~pL-dZ+SkUHT+3RkrOYfm4LtcZ#K+T5c)O z`@BWDwl4jJ6v*T#Qh{TEM0Z<4!Y?L3GE*BGjBO}A|Mlbkfm7Hng zK&c_1jv~T-oD`S|WOBjzwH@9M1!l_B8{f2Dx4;1ZBz<-uNSXHdv{AwN!CRFUqjbFD z&w2AM>!g}8wa*}4HLW+?WE#v&F4ynPjM0zPck;5{%op>Q6G=$9aKE45F^t&dEDa~e$Ihr|F7#6$lJL~z>Y_?d$!)-*t zNRJD`2$laYhTBDiP%jHIKE&I*1%P3*x&YBQrC}NWBK^s3h@MX>sj^Q@4fd?Y4Ohe> zzBQ}-J!Wwc8aqG=?Z zUC~ka~>mJc1551z2QWCFf$kQvHBsOXIk<9icNztUBuOqQ(P>~N)CY7@LkWn%N zHUg5)Pb$foF6?|bL(Nw#^GChg8x2nLd`{gZs%!+PrA7|YamF1QZ_+JnV{p(wmrVFR zbDymm%Y|+)e!g-~UEP$~5moly6Y||7BR} zgv{<9xZ?@jQ|xB^RGKiY0X(dFn1PA3S4GWnKV{f2^e9yAX7+OV`@Iiw&-cdBCn#%Ux4W+Kc{3AJdZxv6duThASi zjGiZRYUGl%_Emm3=PC1Pkrd+3(AUS?xW&3PEbK(;~Uc zIngo7g%@w#x%>?UKuSd6cALPa{4iWJOLyU`?^{q`*gmcJMN&Yc-?>hAepZQ1NBQ{8 z12p^;{d%#_LZ~xe=PtxQ4*`*sl|KW2m#L<#D2Fw+s{%T&7grwlUYg1!0cwQgyK5?B zVauB`Tw74V5s)tYo$rLz)aJk(*^N{1EEiMwftM*Ku4!nbX`dXfjpNtxJH{KyWY(O1 zPSou~pqZIYuj#0^9MNpKXIC?YB1XBRTYLnK8%ErVw|K*f?}2KLfszel0t-hg)oelx zZy2$W0yl-9S216s5%_YEW7!ySua$u7yMR(=T6y=TkB;?&`QM{gyV#w~r{07#MoX93 zU#(L&#Y|Xf0&Uw`hd4BpBP3Zj?pDNU{d1|q3`}D|andOmi3_M)z-43NurKL$g+G6% zK+~7<9!@^|EfFHz1+` zecS0GCH$)MJNeHGfU-)c*ow29d(n3n#bh{&xoQqX0N#ZX7r@_#3MQ4F8_lt zrG-#=Vgx(JdbBAqd`$7xeX=Zzd-_3Z?N{>jU&hddelX$||3KrQ`;V>0D9&?6|2#-k zDK%;ga{i4padqlomgvl;2y*s}Q%&X-4;~Q^Y|^!@S)phv$Yk;QPL;hsDsN%}lr3E{ z1y&Rn&lgBjSEt_H+W$!Mh!Z+~JN8!&x}h;g3@iRO;;J}ye4bJ#-W_54nw0TKkpRv? zB;Php2vl^%?yz|2&4%OgInxeqQNSg-7Wt;d72qeijY1tGKzMC1#~Pi|@b3|?QXurP z`JuaZDOci||1`}##VI^oyi+(yK~9RQqsj;Z)JZc+;p1#c{bu-urG!PjIr8AGQ`$e4g_YUxTse zGUwtNWQ7~qtTD19l=nZfxn$Kk%%I8AP2G&vT5FQ!7flgzLey0cZ>>l=yY*QVY}C~k z=mzWwx4<88KLgReJ4s+W%Ok})%oLq-tl6)>4C}RS4JMaTG{ja9ikfyLk1P0+54ZF3 zh-0T-5jN6e*HyG65t^L=cKFy#hh=*G{2`$-yJb~tEn(TuSsbdLiSADy;S+j7x^({J zqn9?D176fRtduF-+LLbt9g6C3@!*8)d|)sEUj80MqVY{^HGMCwG-D=ncDpC z-|V)HD!givo*|J*843CRdoSz&o2>*^xto74)@YHWT5!{8>0q4c8+cD$*xMF6eS={W z;*EbFT}za(bVDWZd?7^6hjU#xY(!vn`@>rg$Qn=Vt;BI1NmKStehO?blba4Q{!l(Y z-+aBYXl;`o$!I&+wcC&@?7`Re)fmM8Q}FXh_&zT#l;SwruTiv@TJB-!HMtIna`#|f z=o?}nQ+&L6cw96a{6#rF67=rPg}FI4{iT1rdC~X_O!2{K{`Q4N1hXaJgfshICeOKD zgKMu-le{=p77F0mPKjt?G(W5DIXR&_u)c^bkCvW?h{UFqgx+ zklA*d2bc!8xg~k8ibSF1-k9h1-bUQnU0fyJvy+B;!Q2}H;a%V&js0=xI*>f7R%bh z<845?cJi*c1j~lD^P}kgHL1(lT8TO7{0)mgW{!376wfA=+$*Y$enzk8dxU$~(%HOI z#r=)&s7NQC1?sR&vBf6-+&=ys@$s}HtXu2T&f>U*6XM@TFmuv9jw-1{M?sga)Oe*p zFH3^1kgZv>%L>pn-LS#dd)07A%4D@pdz%lF^}D4@U61z}D?4%J>UV4rEvD>v*z=M} zmYy_z?%;&YlVLblgAi);fe@Wd|7kRC^&XHTEeyttmE+)(L?Kj1U61U&! z*to|uh+&U!9>}?^YXx&EKYm`B%Z1-PI%jq>`wm2tzjU9P7}O6q6NSg%G)LZo%C4`O zHO0ACG{3k%Z|CP|ceI1uY7dvb?y#W}0g)O<7cfJsdn~_)-;KaN_gnpe$LI;$(?-H_wLX7CUmb z^s4yP8#hE{Z8F=iGvhCZETo;sP-zO^HU<8(lB^Ekb{n#Dt^GAw!3=0ztkc-B?B-S_ zm4$!@JHB-o{lzn;J~TSI$C>%?#|Nr*z)GbJhvJk>`fIZktxCx9)Zv^SpPwEWJJqM( zpl47f-OmHR?2$`h?I!og)kuHfEEGlx-D@FeGA`FB;qImhkLdehGrd)7rnhKAg1!T5 zmpXnv%$D`D!wl;%_-JV$)H5Tb25&Qnkz* zcBK1$*c^E29>>&WQG_P=;o9Msx*Tn>(==3QP%O%3!6W^z*%40V1M!WZ&)2=|Zi`d& zgLg4Swi;WmS*A(5Voz`7b_`2>JdOIN&L;GNXYfn{|%afUC)cZsF zwtH^g?)tgcIL2=sHYl<}@nnMT6##&NgG>+xr#3{lrh{9%pAF3PQuZHT#eT;hKXb3O z21uxMY~0-Ju}&21N)69o};%c~;7Wg9s>e10Xl-ionsdg~bM1t&oI5W|S;h#jT{E_P!U z&li#vxwaO61TR@Nx9QY+VqOZG`$s-ee}2HD-#dh17que+nb*9zvR90t-d`Ymx`x~y zg}OI>3XCI?^F;dG-#$w0uVcOS&>ImJsk4>{EBA?jiC^cSP%l^Y@jwmlHzHs$hZZ`8 z9)M%uWVa$~gQ-K=;B3uYOn~E1LHQMp`}?2qS1J!zM36$2JRVto{Nsze{GWX52L#?8 z;jkWU{j6SFR#mfdry?C|49q_?=X<)LYE}dq{+%!-miL?3^2t`^2np+w8PZ; zH^w}j|I9?gjY)%2ShPgi-(R||-RtDL*_C9Vo|{{HmwX1ee00-_2QIO2Q~7h-wtMIh zi$@XlhP_*ng#T38U|RLRSEQWx8MHMh1X-uizZOm?B9=smhsuSwQ3x>I(8Jw3t@oD} z`5~os$8i*ftW@=zP6Zu6Vrlp3Kl4$EfTcoA63TEVkagAsU961C5RGr_%s*~IW&N5g zQYY>%Jy{VqNIjm87oxzLh~2x&2wo=4&sY2l}W^--xsR55Oy; z0C<;$L69lmrcU14x~5J}1x3tY2HYq`>p7LsZSlz{4nEaAV#6xIh_)zj+LXDdc{S`u zO_S)nYxE64Y(c-TNwwO8zHeoaDscGY-XM)Z?oD;n-$$uevM4~Ke!en_%>V)w8{uRX z-b+KYEJh;ravPRkQIsai3KxwNV)iRU_>+QDPBT;@3d9$!lyyv^>O!w=7MNmHjLpiG zjRPMgK+GqN(rmTbOebEhqc`KELG`1mtdHX|g-s(t|M9UmZ-x(Lu?}_2{&ndpJHOwD z=V9QylNE8deznV#54hlc8%#*tr=H6Z9+Y*p4IWWWwSQPhlWQms%tj7H(&}6_m+wM! z|5|zWii4g$0ceS)(4GD(qU}+ z0eGS@xOa9xv0;c`X{2#P%!&;o#qs6i3|OmQoKCTayOIZn=aLiLlMA`^AW|9Sb_)bE zVrT?pNl1W7enjO_)L3bdOSh8NSR+TVNk+n%i_zNr(Gn&%pMSn%;Q*%wsv45X$F z0;_k*i!ee%4fn%zXxIm(51kc&9wrG87L_@-;X}E79)OZ(FSyyIGhrQ0U2mL!Wh2)k z#iPrB2%n`>?dW7hW;z^ARMR2l_9-V*s9)N@o;gM0=mfBWaGm0?%p>0cqh;PllgVBn zDe`*~A(~c5_iXE48nFER$PDhOy;wg}txO|GoAq>2=>YmeVlpQ_NyEx zjpF&+Smi8{KF->@#y$-`1NM(fw~l_TKQQ-td!h&?%}yY@@C7i(UsI;cSI>h_KuYR( z&33`q|AEQ})J2d|-^M0pHb-pm>5`P5Qma^txO9?(=oK-s&~As zGBi15OkBiiL&VBzgifVo|E;V8C%{ci+NzMnAZq$G<4jAii|L?N-+*zir=P=v((K!4 zvDaczA8qph(yE2QM^gzDR(!DqD>)xJZ`>@=<Mv$_Cn?~-#4nU+{khAsVOTdugaw|8z2DwAHM#S2i%Hvx0Zi(19gy<)q_nJ vRxc-r{{5wC5Lf@khMNsjaQC0xNir=x?w6ugZGaqw#|NNpm>ay+bBX#tdQ1ZX literal 4677 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 Hu0mjfQGBVH diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-grapes-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-grapes-overlay.png index 17f3be9c262953cac52da794d601bb853d8dc276..2529b5cd1928637c69a512b292d5533fee42a48c 100755 GIT binary patch literal 26684 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 Hu0mjf9r&r4 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-grapes.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-grapes.png index 3dc60464cf31e3ba07d5585542d75d616f63da03..f2bbf26ad759fb2ca151a839d4e61a983ac45f2a 100755 GIT binary patch literal 26684 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 P00000NkvXXu0mjfY&D(f diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-orange-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-orange-overlay.png index 3dc60464cf31e3ba07d5585542d75d616f63da03..254a6f32bca09ab3009cb6d25ca4d57c048ba97a 100755 GIT binary patch literal 26685 zcmeFYWpEr#vMxL$W@ct)W~LD{SU6&4W*9MpZ86JYi!EkmmPNMEVwNqiWa;XCch8!pP04hj9MiT%4zTE-=2rzG#y8yFu z0D$qyUt8Z(6YN9b>h5A~=V(RY>E~)iVdZOQ4FLGA*XG)JleVToUM+B45Hq8Hn2D78 z%uQahtINJ?v^5NLMx~K8UXlHQL71wLvG)2TKKgtp{_4i}(bk-aw>6iw*F1jOr>ElX zarsi-X7A6#^<+1B$nT!V=Y?CrD?yQ0joT+J@+}{6ufnra; z%aOZXaGI%R@lU(e1K4q%Z^PaVHovgh#UBNJ?~t-{|FvFg>xxt{Tx+hW%!WOCez+a+{>&XdcEGbh!(8&HFwl_RJPT~XY~BZuVWmm zWNQS!=PLNOYp;Ry_4Mp%cg5DlL%5z^g|C}Q>*%)l*WHT^KH=%0)(K(uQKjxZ^Ca@e zJD1yURxLY{0FpeR96{^_{Duo8SmEt47JRX`;y=58F;lUw{Acdl7!E@CO>zc2P3p#PsP&b1x{3! z=e-=ae68($eu?_JdCD6iT895Yt?v4prWo4}3_VC}5d2*xdNkfhLL%UCN*BN6>uXdEUb{5oL3i7oBa+K{|mj|pdT$|Ts(E7 z`79Y4P83fMug0Bq#={pMXQQSNJOB#AUQ!NbO2M$?{S=$&c>JXB*G*6BWC2sTdt*-5 z!*s$UGn9!iWW(mhN)1BEv0-hb#1AI?75umFytIBO$W+mQ+7}WSJ>Zl0<3`a@I;4%= zGBKi%2kiNCl1G<&seof8Q~1r6^!+9rR1@n>d>8Zy>=68-MdZjo|aIRYnXjV_Fb z(M01*eVI@#g*e)%cb-2vO7 z{oCW$%Ce!JGn1SjySlwy%wB8X-`!E`vFJVXe9qX>jpT0YvQK1^l380BxXMr(^`7H> zin4>7JYi{)Plo1V9_#C&5{%B+3P(X;cA> z6FA_bn<+rIVKDXG?&p`1U){~du~6n4(M1T9*DL3{OYay+r88- z4KR5#CUo1K#G=}7$LX&8KMI-G->T9BNRUnK89-976=|7wowbM<3Uawi}rApe{c`L;Ao zJUDq-E8Kz25ns>6PN1(P{hb4!jZCm1(rqaYlm3B3^3sREwLFhLxsU9Iut>fx2o7Pt z{5Kmd+P4+zq09ipo9_Xs!UY)lrPp{SsgR57AE;n1Vdz3)d!9|Why5ueSh#q`wRB`N zd8nY82AImyPU@=u*%gsco(7_6`F`B`Y^P*S4AD*+Be# zgA)U_|1>fRwS26eI+TEAR(TsXMaWz{+LkNJ()IwJRqiy@#+O>=>a;4!^M(UW&<_P~ zZRKwj;0z(EM69{Mgh+RA(Pb(qcIkn3M{^Qw5mX)L}I^)2k>@xsSK! zGjBHweW#zGyU4})(B+nB^+>_t_QSn}u4YE|E0B+R8zHGcwn$XK`m%d24P&zzZ}JXj zXt59osX3C02ShW8!PpaU5v-&kBXbOyb93+C1sbW}(V%G>35V%9J42u}=?F9LDD6_wTpMD7sWB%&n7**h zV2Sc=?+G5?cSAuHFL$2$7wh$3#hccjVEKOxS2DS_*ymCT@0Rep7hu4|;VNU%8oSv^ zx}-cYe1^N8-c6!zM1)~78&c!I*5_c}3x1&2W4c1-1zTe_t52#@=b8n28NUoUMG%a# znn7a7es>e;BTYhHo9k_0dk{U7^C{6cKDic7IvQiA4P0YO_F z6;RnFr^P4UG^xm&N*|fro%Q%HQN}viNh=1DIPS0+EK`S4>W{Czu7{yf z(fff_4%7zy@Kl!#EvfH$lQUCK%AY0Mit_=#vNLdu>P1@Z@#hg{ZoOsribyEF(<5sa zk}MR#2wf?txQ{ah--5m>mv0Jtm7E;VJEUU30k>D976S25EF^0I%?!G^;99iW%!=gI z@^o=OR7$32eJDbTV|cCj)z$2>z9}VqsllJ6Kcw*$kdY>(@jurdma%-7Ja<`+cbKJ< zy-q=9Y6d|Vx}ds0017CXK4{++D~6fnh#O7bN8wCM&>&{_b&ATd$D3*eDXt>qdc}18 zlqJ)!sy*>6!1xr_>K^XY@9h!P7ac8cEF-N%GJdeK&3l%z!W1mC*pP*yc& z%wFZHtSo>WyQWf?hxO(&0&9c$sIjjERFZI>z=oA3{Lh*r`ogq2s%lS;xE(pe0i3z4 zn|%}(L^WMSPXH!^)D*cBhxRoL#jL)f0wOrE9*!3zzk7DW zXw7IJ>hXrkq%>vo^8_ME<4p(P82d4BpkK#)mFo~wKYf5(9_$jDK? zc$4NFlcA&cS`hOy!biCz)>BE#;zg7&TTLX0Z@i58HQrHpXdl@aT1sOD=k^LDv)NS# zBk-60!^y8%n7}D2`Tk+uxzse(AdQC#sd$9&HOWr-Nh@71na>eJG;?x&sLSXXJN^sE za({C7Y!K%kon0Kj{fqjj!Md#bEwHQDg=~xqB0F9w2{h!^yjP9JY?*k_GGAxr*htd6 z7?i34%JtLeGxnr7b;Oye^)~uMq)4b>!b(jDXc2#RtEf24cy&g7z(qfi*imaC>wxd- z!<}GK`J?)p8fB}cuixo}EI?LPsHE79r*hX160yjBbWXOaaZo z*+W>lEgdO2oLLj)+T}|rM%9;=XbbrSfzN43uHLkby{l`(oz z0s53W87LTVVgNSiR|G?dWrZ1qyEf^@LSASIjXjBbN7ej$XqGBUTs&B=EmZ`DDrdkH zI%YyA^fthSh*K?hM`J+y0_uw13sapmnhJe_1rmosCql(qX|KwfGqY7OQy_!yg;(1a zK)+yi`Y<5Y3KH4=iX$d#5~h!KJ3oce%g4QUGKZudt6hSFqmB4gPC%m;2=6OC37Z3G zM_cmVWIuce{SYVp`o<)i-!1tj)!ECe0r|kP`Y5`;8Kms_cEJ-M79FR#k0&x2R06 zD7^4z%|!Tl4}}`AzKbX1C$+=KL%avt=LYm2z} z`*i!@hL~nB7feH+pNu z6`UcJiXT)|X+`TW{7Ym2m~D;o3ZH6ZcKZ$WNTz+jv7)aU2WJW?1EV%wgD}2hD-#4Y z3rsK9abBi>;XhR?R2-MK~csP;fvjij0&A0smS(B)H+LFL)B6kVFoq4 zD4;!7NKx&3d06>i2$_udgwf@5|4>j{sw5P@;OmEa0Af^rFGDQ@5=qR7?O`W;9V9#i z@`=e+8m?ru&VT|-C$Iuo5{3IgaND8Q41Ebo-z*>z#7zOzh8O#-T-a7} z7lC(~qeey=Xo-tk6dSM41HF^@ay1KwF@KmONxA1s~?M@tX#}s2>C1 zn7b2l6?CINL1K;2@U>+#yM$^=4@J~A9C7sYgBs+5nv{re>RVmB4aHks=-$c9V71CR z-eil7!xPVWKv-eqDbXR=Yx$k~@b!+GD@vt^T3~S%*ms-h+xv~a2F(rpK!%ZR6}%s~ z0+Pr!g}S?FyZZI^G6x+1TtNb?wVZ9}n9}Eur0e2hIU)Z2dIfqTQuX%V`}?~)G5UD4 zWl#0Tb>m00NnLhn(+q3k3EIw2oGGOZAVhI8uH|&KL!V z#npbm1Q00HOss5{PF$1eJG({{Tj|nl(AK1CKxT<4>D767j-SV2 zqyPfHEXH}cL<8ZL9WDm2PoTA(3H)?2N*_DE9tY)3Um8A5`>LgX-VO-^HE`oXHu~=J+-++Q=mm0v2_}Uib{o6&rMK8t8Cz7Whi| znh~^HqwEDPi%lqf{Y87*%j|i+Hmi_Q}NDTzu4-5 z5mJYsl6nojNk7C6O3;2TMFkEfzb_^U!Q>dOQ|_UR&H2O*Coyi?`4N1KEJZCc>y~8ZD^@*j=rCoG z?}wHlkE8v3l2Y#{ma`b+b{_T+ZH-7=Xi!5{nV2Z_Za2urPA#WZm+{`fbrP3nYcT+) zfMw{te@Z>2Z{TmQw!Y^*ParNZ$cOkhM5F~;3iYlQzlV@inuGh0P{3%R{=TJ;ks)h?z{^ET zACmOXo%9p4ew>Dg1Rtdz*0e0$?Wl;795O}zTvFuN_Ss3<);aHnZ5-T(!&7k3#dk8Q zGX4~4Y<@)ME6Wp)17BUvL3 z+n5o~W4hZ_sA8Gc2%#=AfwB*`Ds_ALs*NI@k>jnzvVT3RgOL`u>i{3T9;R&TiK)6% z0$siTyadA#F(yL1JH8pX`U1Fm{0_xNk_M=ssh_z1M{cja7})uK9rVd|&#PKqiS-r# zg}?V{0Diqtg9xsDPIoV%#pF`b1{F&~hY4Y98}$qSwk!&15t`bC{mo$8J9rS-AFD@f z&3pTmXh>$SOdY9j1x|KxcH>x>xw@xT#4u z+*4i~ZRVTKa1n^tVc$%=*4T+Gg+Ydn3(dHsft!H!ELOpD=k9Rn*J4J{3XFT9RGs2{ zYr4$Rr>4l!8@x|7CEzF(m6iJ12nfviS4#!Cufd$I5=)Qod61&xzEcG=?j(HTr2J(M z1(~QIsIQ7aa*>rL=XTwy_bmsU@|#+^y2p<>EI1G5ghI}F$%vNjT&Yh*iB9Hh{mtX3Cp~?ZuGy`_Uw4`w zr}3qnlCR1S5`xFpH)NYZ;x)$gww%sMOp@JGQ*$|G_M7~<=f9^pc|@>x~(^3v#<-}9d2;;`_EY$LkQ zygNp=eWdVJevVeIo690CfKGpRWAF^JX(-e8AUQBbQzCGC3x zA=p7lEw0$VJCK1Hz2#v|BuZ&up3nbmm1HDzS?;j5(ZPy|QtMaTN)#~eGwLs?&iU;-cL^l^0G7dIEdpjTz1V~ zLWe%$nWLbZW?Yn~xZ4KI>5(>4R3-SkVT6@boM^+L)~$_{Q-iFO==amH%6U~LvK|g5*P;gE|(ng0v>;CV_zw?{nlhhHQj7Ktt-cxHcg~GL6FKI z5=cJ85wQbctq;xRYA0}yj?LmWx)>&)LXY8E5!0kOth`qEn8NNa1l!APjW=s=k?|l) z8eg=+qY?~266@5^{VNu+mYdblg_xHZM>gwm)>p`A_;gP2c|=R&pEL&vGqfkg1y*A| zOx!&*B)qhi>U;2MOEv=GlCy z_Urc0r-gPTLpvRdhz1_`Lj&GE^pE*6+s4L198(s1-(-F|RM=yu_~2hVk@V)ow7Lae zN+wz?p*hTXPkfum7~UD)akf%T&8xt4xOKKl(tTu3DzBXqAv%*b%wm*`Yp5aGCA!F% zaGU*p@=#=oo9%ZGQ_#I!kgektQoelSgQ}89^g9R5W#dF8y47Jh-?7cO`qTU^DGguJ z%rl~7OHTGx>H%!bKC+Hv3CxUr0xp}3u05NYRzyKgW&coPE?hqx(J@+is2~PuBjw41 zMag%|s(7fBS5JjjAm#!gn@KU=3u)QSjBW72jHcwpA(ly4hp^q$)}(t=Gpyi+TZh%C zHgnpTME6C2ShPOe;~dn1I#F+c2!s8|FSQE0p77}{sp*H?(Co)N%v*$*ad2H*y8!pT2f43fk&V_TRCKv|6H8h%hwJN z3^;$g^<06R&TE4RFB0*==xlbK9n;0dP|oMra;OoLFa`zO?gLvpJRba3ol;KEb9+EZ zI)z^-+akB>*CXPTk>AfQpA(JXl$F2Fu(WAD=JseHx3M_sDnw2OLyyyAwl`;e@%*mH zb99H+mnw?}oaiaH~etm+1$bZXvGLXst8b0d&V_7mT=PVQQ>JDn)&& zihm2k1SEIcb)5xFJ)C^m_04)b4$S(3&s%j|_(}x( z(-)ESr!rJhAt{DU0jLkF8JeB*w=xtFyf8BnY_kHSPpwiDCZyfb)cntE4b`)M@ zk^;}!eFBp^FUD|+k&mw1OlLXKQs~#3T_0ibgGZ?aHA9KZT(<;T3UUbLhmQ2{i&mJl0N9=FblWE)kS2vzf)nOJjkDBV4kd-aqgPTSLO8a9-4 z!bDv~qTU71znUD16%U#lOpw zpMx8W%(nx0hi6YwhV?ctc{nc$r^HROU zVbU2evsoz@X20-Ksi)~+CcUTb9~}CL?L<3RLb*aiOFA*YMl`E1|ggGvXI(q@xmCMxZ7uGL~cS}f{kDuN{q@aFj< zipF}fsS=vQk=aY6%yc>I)ZJ8{YP~M@y$_tOVeAjmFxaqWc13btXmYp_@3pua3z9F3Ok}mUTx@Il z5>X*_AJ|4WbXlZA8VSq0C7m+}qG?T*&RL%^wS@2rwZvLB`QSLq2>oD4gFY_m`%nc(ijzYDVP)s>!n7Hf zPH_;(EMP`)%9urxg6bzc>Bnl&CbUVQ)C#j8=6v}SLlRkH@nRubzIV372=eR~C5dK`5ly}#OL7=$oQV^U z-eVE39CWkFtceOKOFDX9`xQWSWDwOAyR{*D#<3Gi;kls71DknMdb=BRs_6P^l8Rw) zOJK8pxb!-qe_nv%g z28-CNu#W9aRywk_+D z$XP1{y;sa3VWgv2VN~$U&)&VMA5&%Ofn-sR)8y;r#D`Z~fep%YnaJtuTGyz=XXhP6 z6#-x5+x?SG=tT+S0H>Gp4p{N_R$e#R_Fifx)vw4mN13CC4Ody77xu%4`fW+bs!m^L zl)ICPX^?;Wpg;T!E}uu+MImjc)m!1Y&&;wu{<=uDIHJD9GVd{!;HX#p}{#KlU zb$WuPzURw^EU%w1A2628l>DLM`OpeW5IWCz;25JT~H#g(YVb!g>&8?;|e`H{rZWtAAbG`QrkozpPgJ4gCYZ^lnTI5Dv@LA z;Ls%9qfeK}sJyjK<0c(XEy=F}*73d^;lQ~!*h_0g3VhQzKl7Z5bd-%x>8>GjN1!~@ zPXlO%lZbvO6q&{BUuFAxC8l6WiaJ~(b`c>XQ-cOB0mA+C0jd&d>&?m!{suJPt$YXm z+)Fr|3HEiyP&j=(%Cp00{C2!}I@fCva!@_K_9(IhJ7st$7Fj0J)a(zX(<3x=S_EFd zys+m#VwQd&;Rq91Hc}uxCM#dU`Q=A;cV%?mzB2m$`)QY6`$rCcwh<59_ZQ-y_js!u zIf?4qms-05+I{4Ix_x>FE!5}&RIradCR*y8k?{o4+V|!+M$>y~`kxGu|po>b<+{oj>71<9m<(dVD z<`8A4Wog0-3(n@2;!$(k&%FS^y4k!KGMU7EU+R;yO1Y3{;Z43Y!m1g%C~;pptoC;m zK?xdGCbVT}LiUNczGhkaOlD;^#IS|=hA-+2DVMhmT}jhYF}F{FHSPsR90!49$+G>+ zAS+QiDQKwCQ%12_=#KnBcxy4WBm2vpi0j9$LSZ@23zI>zVp03l%29mJVEFLrJ=#8) z)hKi-C7ul1~F~R}3T0Q%*1kj%IzIb!*h(FA+dJk>_F)%D}Biy{FpJkKPSG zkwZYqD|~YK!Qg5!^Sig2P%1lVX?2ja^naCsz7>Dw=A|?!4yxls^~%gM2{Dq&&_p=& z53922P$J-(*O+-JrsKqeP(X^;sDao)nP}nL%x%m9Pg_UZW_au*FUf;~uOXMP8=!$^ zqs<7OYNW51sO-^tGdIM{tIcagxDc!Fw_Re1hqt(sM@plnbxoqW~@;uE7>A6PkI9FGts4h!* zE2br}dn=mNS5X$SaB*S-Te_HAvH3c=z7^F103zbPu3!rXD^Ch@D;qm!QL4+%UMdPZ zOHnF4UKI`%S7|F-I|YAtD=mLjZ3}+~3qeaNaWNzjU!gYuCo4}dg|CyNvxktcDAnJ% zLT~qfb+c1Z{4L_?AWEgLqD~?0;%-I3!^XqL!7A%(=gmbWhD0IaZfPx~DI@n!h_{v~ zm93|zs}MW8kB<+V4>y~OyA3<1pr9Z-2NydR7wek@tB0SnC)k(O*@OBoh<{+nSb12u z+qrt$xj0k&g$Xuy@$wXyrI$UhzWziN1BzttYIYg&1@c)43x$$DEkds6>9gr&v5^j*E&9sdr;(t_Q}(aPyf z)Z@)6=l?J%4^mP8m&RWd*w{I_{;l;!_WzLdw6p$ivi^r}e~tVd&c8eIrv5M7{~`Tf zvHvanCZ(bxB;#V?^_P1f8Bwag>9&1KHZXU)ZG&L_yt%EJW)vkF>T z^08X-TJnQ=!B&Did>sD<1#XHT^TsUruS9(V_*>&G7$IqQE3l`FyS9sqqbSv19#H(H`FDO( zi2P$#6zn|SB>etL`F|$8mX+H-TK`xBj&^?!QBeGyw?bfxe;Dxqds|ulZRkz!A43+l zU}qbvw*~&sjQX#ByZ_5-@9NgL*oI>2( zLR>t|9Na=298~Q8jF|neb^TuS_`d=~dzY_jWcK!cG7t()!@>n^) zy$1Qbeb9KXjx@e~3_+PI$;$u$Rf*JGZ!JVu1p^NN02%wQ9}tk8OYqhS?+H?og+D}u z!lNWOT=K&Q04M+;8A)y5_47QCi{V?EkEPOx4)ktrQvr0 z(0yFRB@(&DCK)O~p(q#IXi!U~DH*0L1?6dGi+-r4fgV|aa4e*0IRUl5OwUaWclA>xq*m>Q)!>8IZA;$k2Ku`0425CC z^S$G+7zjt@R;Q9r<)TH}A4{Q=j>F;fj==!uIDLstp^U1(V6RJvvP_3;_A=(Por znU7?(!D5i9M&NpPFd81+S)|Q%r!P$9(XKxn5!*WQBbIex0^-}jt$QZmw)^S5cSj5+ zwQ5|#SSr4s`~FxOrF?#)wbVF4uzUX)jY2)8%x>Y5z1bg6*9qR0SvAY!UGY`Sq@j(qmV&?;AEP<7 z_`8gRA@e(L!Wb-w;PQn#%HTR zt=0|~Vbr9FZJ1{A5V1}|P17vWNR_acnC1r$CME+Ye-nTqV5v5fg?47|gE?xVFtHVx-44JegHHXB9IeV;+SSRrh|1Zxyx0Cta0V{zXeiX_H&wt_`_ zXR<(H3BsVCfx8tkkCGkGT;~8j+4W$fJH>rZ=9D7u!GBj&SO~EaY}_ADr3B>?#ERC5 zsCcUZw3SmhC8Vkrd<+0*aey|rz5Zx?el$n@1`Dn5nGg6zh6%XVSsL|b(J;qmb|_DLHtPt3l@+;1%D)Z=Js2K&I)EGsR=ZY#QNY_Sri)~)dP(BemY z0=k$}K}lV?h7%1weVg~ZE2}h$L~h!5+Si6Oxeti*(&*iui?OEH1SGSF+FFpTy$|6q zn3_0DQz<@2tJI-bhXI7!ScihLL8Y>27`LrvsSkx-nD1`36ej4vOd>0RG z37&rXwIeeS1r4v7`*5|WA})=xkQlP{(=&?&R9thFDI7Qpg~<5&DMdoa{8RO7!@OLI zm|Xjm0R7d)DF+QGgI_=6E`H`5}`;xnX|wBP3eF~lN# zW8KuS{v5ZugU}2DhC@8VX8EAdpp+@-?2!}T*cGd|iy8PX@!Q+tkdM(a+K1YM)Qec4 zT5%1n(hd-*`A)Gg%akfxyj7!O4M>`?N32B@@C6FwgEwJh`Pye$du%?SnlG;nY~6HP z*g)mN2oCDZa4W>D*sM=b5%T$tp(N|m4Y0W_w0!B&P^{@#Xo0BMDIw2|+KmvDA&+wJ zb+L3hHjeRp^>bZXlbXcjF#eNl+&n+XNayzvb3GqSK56;p1|719S=1{*vN0o!D1GM`!in_1Q2XtYOl=_8nfx|4b7>wH4Be$`}X_Qy9 zIKhF^L))=y!Kfi5lfx%6Ft^Tt>DF>gx8F%C8{tZ z$)g`A9|>&$rr#1L8wQaU1N)tH(OTVo`!1gigY_NDIj^moAKQ!#$gH;>u~ zoTaK9amo{#t0{^HvT=NlK9+g7Pul>Y zRNG0QW-86?<~|;}0i|M9hu@9b^DY7Bi`sMXm$l`{0vsx>7^edqXE&hY|+b`LteT>4%tNVrYplzDh+IK>=rKCyaiPQR-@4N-g zo!B_xmMtCS>$Jm6Xm=GELF4Y_L;@ZG6?E05t6_q0#o zXyeMBi}*_RmPwgMF%7Luno6;y0)|ZtT3%%{)<6;|vusUGER0oqqFbC&Vie$QqZ}AP z5I+zdIj*(XnAq3f7us@5mi;o$&`$MT3cJv~Pf{2<_XpP{m-?syTbg-dfYIKi2p#=< zboV+m0Pw?+UdvWOGBCI=S{ZBM*Du5y83S;3UE6dTPW20w3-*%(SCmv~nH8qRM;ckI z+(MN_4ap~(aHfU-po-~dNX>)rb zM-E`686D3VJo1!Fr(MqI{?V`JuwWDXUO;we!{)x=6kS38YGKk(Y@aGZ2>xA)O}c#G zv}q=#7e_kC1RBT$iw^#j2>yH{J+oMa{B(21F?(-xD5SKED;Du*`6U!XQ&J{2T`GmP z9Jz?r(bHU>3OE&8ae~~XYO>7>vu&E8gNW@7_edzdk5cQUI(P2+kvMP~dH`qXpnkLG zPnrwycqiq4PpDT9)c~8GfV{%38FFndP$WYkJ#_-ggqihc9MlzJfzIxWZj|rG`&0Xn z53FQYs9#%-mEX);YH0uvQ>|lJtdKGY%SuKH((18>;>@DCZq4-94r*w|1(cpSZL0{1u1bw*(y-Thu&$) z0jvoSxL?&QqKC@==Q;E~v#)OPg(pY(mX<&3y>}J9U41XvpAI3WF(qOtu~Emen(@jp z2D2iW3@Rn6sPO8#1@C=L~!0#kJH18BL^`raX}o&$#-i!cZkTlCVBYhcBKGBrg% zi|oyiUja~Md^N5><7V|i$%m~a{Q*Xn2(1UHT*QIH z9|LE(?4+O20k8`!79+}0b-KU#FOBR*uj$|!YoH&xwHX1`mdA6r~Xg}kRo?;0eO%ATu}$c#4H zqeIqAC@vMHx0qut4uz3r9HHWGACdmDl(BB2K(NoFmr27=qi}Q7q=tqE6w_+i3jYWe z+YOQu`hMY>?4Zm^Kl&%?j+_ID#y30>fH#A+GzN;f$8^M|ro$Z~8%0}1#M%DA^D)y> z0s9dj)Yzxud{vtEi+eSs53@fh^AJwgNT2r)6dN3UO|q z?!hRdYwqUPZ4Cg8OcJU!0z)f}4tWdr{Dc7FK}-YfEWR%$WqsFBbSr1SFbeiuUr0Vf z@&o->1o8(@v523)2@Z&`&V6}v>oLtTnJu%@R!p@K3$S6Ib$`9641m}_w#l?w(^+RR z+_#VOD3zjaggh`vWYd0X9}g<#-E*cr^Ylnn`iVeKCrI>geu~(4xrE+=X$gr>u$yUm zdo&GfJ%;PV*|*L@`fH^N5tlKesj36}w+t#1GBn3V3GfLhV^pekx0!Q7MV~G$GC)zs zb1}-S+`z6(#FVkDH@Y%d}16ZBXg$>?=`+YPG!9 z=Ki39?nv5#@lf`tOS_*XBoTs|sANd7jP2Y6C&^4hvo>?}$<8b=>A2mLVM;W-vchCp zrlqjmw+=h?R9`%B3I!#H-wDMiX)3eakU9tQ5>SAthiE!$;wv^|*t^}y`ocwi9M*ig zGNc*D`udRMM16Ztmj;aU=$UR^C8VeTp|-kzFurJ%GZNN$j8yJ3u_(@w93fa~2`bAx zWaDN@=QMxi&%_?9p9;F*QmZ-u3g@A4k5D*k4A0}sg?6766vP`*RS;V=5378>&FD&f z3K&#RIb{6$er;&kl5ULF{7j+q$qJVBNg@Fxa0Cu_d*@)nKKROd(Ljwzefb)tArV7* zF+YNE&TB1E{N9a91SiMR_h2XW&kev^3dJoBJ3HP;lf}Itx~57rq=0zS7y4&A(z%_k z-&hbHk9FTLqAI71ZSRv3%aZ^BnH)j68Z>J->BTY{$Mfp;2ytO@NP@n)EYzh_L_m(&PiOXxa*}WYARiU($0k@abj>$4IsBPydPPT%XkkZ?tn?Ex z3M`VNTePr@z4js`T~$snvb<-UPZU~nJ#)M3(`B!66zT<+v~q9`t;IcY=og)l3-^Lf z5!#YtOxrz?L%V91##72rnp}v$Flf4=4&_t?9H!EEv+DDID32zMUw9!Qdx5Es z{c3rN_-Appt_2@7by1sJkRshmF1!#I%@Q+0-|7KhVEVo9Lkqe+7wT0aFw%<7Z&E#k z9$rY`t)8Xj_?NZ>^R(7(98q6=k!{l4#he;Ha{mXt$Gkw?MTyM)!hz8S>%7i-E7`TA z+H+v>DX1hQ{t6g57PqPPRZf7aDR66?V8T83!f2-6*lvt9_8}63e*k+5C1AH&((#|^ z8^{uafT#It8OYQSlOW1slisCt_P8Hu#=WYcMLKklZ*R)2z+?|Dn!TWDW63Z1AY7i1 z@L^i68-@B0ZQK!_es2~W?bSg*>PfO#_Z?jY{&9TDq|lQzo5hJeK!}=UpRHOZFZ-iT zZEvjdriPjrLSLgp_`G;iZTkX}A#CK0tnrJjxTgL<_yr}o@t;d3oz>&?a?M7DGv&LU z1ojSnkDpwh&6-hDGU#Oxz)y>U1e@gVufy)`jBirB(=di z#^;Lip#0ox=xQH-saVx0ZA=JOBTX@91u72${2lUi zGigh{F%tpj`E-*iyK>NU#1FUvooc)Ytc;ocxr5Dtz%mpcPKRScuY_i^y7u=#oak{G zq1-0?=MiI^U@3|O_RHMa6k0^`(Dsl2qDN-|;PP31hFb>h|GHp!AgBb|O;wONqU-SfA)0rlP*UgfI*>CE)-C zv+T@2v!r}5Cli=Qa88x|g&5&5m1ErjwE{yEepO;cnW(g=AKO$`jHk0!Ef`hRSvLmo zqRL~fwu*ZZnWsS7)w-XyVgMEgfZ;WO_=zCueFZ&iOs4kRuT&wAn+0*ln$KqHzUk{p zUJY8StD$9J^4I4YC@}(LF}oHt)?y4F7j^r5wx46b7qMstFeSzsU%W75=B(I3t`>j!EWqC;a6tAtp?9ab>-WEP<_ zra*C$s4oGJ5oR=65Y@BD?0*Z*ns}NEFc$jj0<*|!m55QPS};kIFv@DJ&>OX|+H6Ay zFt&iw;O4nWQ{cr8mFfgkjG|kz^c!}7fKZUPFJM7a0Z~=$eJdH<6ii!E#Nwx~hkv-Z zD~ht~r@Y|(ut4|)Rpm*8k3Kg7__aWZ@@^ci@RC}P*MgVXKIo#Lu>fNdQ41qJ6apSX zi86}lKu1!=5OAHO){alXh&PPAi^;KD^zMtnay%>y!TF>xZ^ZF}aDHIagc*~32ePri zyD!m3ncnd`?d)+T-)y1ni<~RS!)f2{RMrm_>>a3lB*cTWx3L5SSuImtQt=otCK|?8 zusashriqu22vbLh52lM&7w}C+r9OlLJNSW88R8b7k&xN5mPgm6%pB)QGf_GCc(t*z z&_Sq}Mc02%em%=)l46srIHHkj^--NlxE%|CaBwDNqQ)#ms*zSOJ^fgK{0)E{f92)s z$E1?Y3Q=3@vLkmE4<_5^SZ@T6VbDpFjU2AIMJ5)XF!C<+i>4mcE* zHUOj)0m@A7hWPeJ4iKQ!M935yr1VN}4D&CwDW!)1{orNq7%%5Q?U)C^4*7)mcE>d9 zfYyUp1!ng^C0o^e0v1dbPVf@*abAYt(JEofF_M%6&aKu1#0*A;Go;Ps0krHJ+~yci z3yQ#jS#XvrXcEMIOK}%mg6g8SjDojAW!!+7Dd#XLYlLan01M49f3azi7900+-(d{v;V}%~ zLfK!T`$&s@NhnhSad?dyDU%yZ!?d{=2+y=7ZmEDKqxy(39@ivpc~2eMT9x>f+Ux45 zu#vBz@Q7>B(iDIR-QUzAz_!*IttVlRM%(2Uv+qO1{KRj2DcD>*D|Nnmhrk3*x%D8B zYk!R_J2e!iZ?=5$Kn_r!CQV?&ZaaZG06h7##XKeHnaZZ-NB4)+0P=xI-&8KvO>Ub^ zU9Y@3io&l+0T#bZ$W@<85Zz$JMkwI%@|TI0_@E+x>)7uNae8C&fUx4{gBeQclR~@= z0!tH^H$(7@OxQhJ9mjNeJQ^p4zHnaLZSuL?Sx<)e3$Fiwh#U)J<6-0+!R zWdA{@D(T=egNcsS_F3J_FO&1(Xvd}{1Ax?U(ax=TrgK83u^7;gy>Auh5p`4Q7wHHt zaq2M(jS69L%Ya!LKDt`?q%vu_6;ToDb1=-SCDbk(8E6=vmZNytMY_p}Vkf;eoV!{6 zQP$(wpP`l=e@_&^ za#;TO0gM^sF*HPb8xoTeE%f=f6{{BZBH}ET^2Kq_!PXClMnF~)28D6ojUYT%f32!K zY>Xd5vtH#F-i&9zY!uUkfa%L*h}R{83x7>V+dPzbmptYtx@fnIx+4T1=kUrbvjtxR zfU)*l(mqAMeywID8-Mt38{hY3L0PZn>w4MWNp7hlNo;hf?AE1Eky7Q`j}o}WIQ^#C z8fX=kgM7|fRch$$*RU$1vn#ngpyJ5ggxdxN#b{m#h;Gd+Ajzk%=Uf*^axFC4D z(sGP}U-B7$-bI~EbEeKI#Ji^Ln!8-1x#`8mo!N25@rEvb_Uri)0SaPCWK|1BRh{z= zcS5 zTH}r{Vw2pS)A-JK|L$9Rc6{m_k7TY#N-N#YZ3bM7&7XB1NE4FQE3gX2 zH4520n$C7Z@GBHQF~@U<!Tjdg|2uD3OsD%$MFu3;_8T^RR8=>BGBUh)J0i{mAl7 zn*YBH%N$cUJ%YY@0rwC+%pXbIZ^(B6uy(mI8mf^YheJzT8T zo_BHZ=RUHl% z`e>>Ld`IA~OlOBe!P z#iP|~4iTo;%(zIQ>!Q!9Sg+6teLByvYKpkiM#T4@L#ZpRvU}ZE*XG{B@3G6>oX!^0 zuS1)nWy>5c*J+qxC#|)BcI|D$T$(Bo((IeJE8?{OxzJ?=rm>;98I+7Ag;dYs@-cC^ z7Yw_iAHUM`2E{E>Gj853-f3b)%DrO69uPYIoQ)jH)60!+7{2EeJQ{AUF~d1Z8L#c9r+=-W#Ya$LncH3wnMiynuZeIZX?o<5i(xo{vrE?#kJ$^4Qbqe8;XdUiFdRBZ*7EZNYdy7x!* zbxfd&m20NZDq`tukvx5Q`pu2~_hipFkstNre-)scn)9S5#s5ZJmc&ggQ1iswqa2@; zG9Dm^;2dP~O_PKmgd6S~o45X4IDwEe-H?mkWdVFVAs$3nlj2}WG{JF;$nCqepiS<7(r*N?@3*(ZlA zGe}+-$`~^>mGHvmNgP@Bc~S@YWhEf0V9Z*a#z@ z<@_YnU@f~XczA}`;l{S>%$!J-{r4QMS+$O{7>aCj53`N-x^($@bA*BjZPoo7tI{s+ z{gwrrbqxi2fqSB@@O!*ZLG*8q6F5%u$gz&IMQ2>=4jV6?^x3q9P)ey9Vw)#bO*>M= z7ktWx+k1P)ani1e8tZfFA*{&678ig$AvV)-h0!2?SftE;Ma@QARQ_WYm)b{?$KwZt zgx=6@-9P!5<*k;$=e3TjWlA^p6k9~p4II~N>JM1K1y9CwL-Y$vt5!*omx*_LRbXWu zq;)lx_JF(Bd+eeLFPo-kNM%xILcRUk2Rp!JD}z;U=iiAnULva%Tz6hRm|*z=-qR5E zvBS+=W7>lF5Z*@DlBBHM(FuH?h>;87Jm-#^5jeep@U{br<|7AdNqlG0w1cz15(mum zx}#hGRKVXaU%xC`$Fx^E+8%c0Cgc)l$d!F{CdvO4{5&$D-4yu)w9i2#B{Rns|;aJ~+wWIoFI}wE`S- zXWz->JF{vL{al%&Z*1H2l`jv0BS4u{`$zDPVvV`BDZS<4z~?B|@%b%P$QHd5{~ zpvL&$w;J(@$m{2?#d)=#09d^gpG3~TJJ%)OaV)w0h+Eb;m+xZyQumwUh_@a6tJ89B zVuWl+``z|u>&1FdK6Crts6w7J7ge{cgyWZx3fYbp2G(fD)u-}1EiT_r+j>+h9K!NO zzXBPE;29ZIw*A%tw$XilS<$;9QDmhr=9ziJ^Pl?aml#zZ2@1{_AH@HZ1e*(Gz zq>QQ8Szo_Gsg=svnB%U#6`W3{+H|?Rl7?ol#NBY@5N-P*08&;OL2leBvIE3Swh zTlQnb>w-m|kvwtc=#0ygV>(lZ5Niy85Z$eS86*6zoOKof5Phy%rbcFjFfDhCd~n2? zJRm{WQ@+NY<=wZ3nK{@7tPawWx41Z^;)T4ZJL@>xtRdJ=pVZ)H&^?qN_bRAXV6#MG zGw0xBcSBnQ*J#manI<`E@M%TB%LEM%ri5SczqrWH)V+_REjP_|PeEv9DXz^sY3zG3 z(C_~`qE_M3rVzVt^ZtBKUQrP_VyWD&d0=z9SnBtQLcOx3>Fd)W!4MA+t~?5KjU8^r z#{)OonZ?pyX>lKjg+?aQvP-~CztKIpuoG6K6sF2LcRI8(AO<~k*8PNNWimZ8>7Vqd zYl}NgUC7dlhQGOH_N&&V*IbsybW^5E`Dc{2_4Br#9P<%AeHt20DEZY3NO*6k1$jQ# zYQc>8L1O3OmBhW8tGZ_Nik16-7~S9Vry#xrk~G31b#vMQB5?WEk$aaxtv&WOHp{I3 z3nHS#{dYPp?jap=#Pf?MYJU4l!My7CpO@xy;kS>@Sl!LP0x^_NJtwAy4TCNu(Q!E4 zk&m#7+bdQrN#0ehPae-Y1i3n#>|r-L!ey^IZX%$}Q`WKPY3}LL7b5TNYeO!6q^KtW z3g3(~^_rv)m>0_#wD@-GL;aWj9Fn`bm@WHKmegVQhmIRRRfQKE`+hhmOO~fPS>in{ zbD?_0PTZ}1s{RcojZs-!thSu2go|NIS(kBin$njoq5rHT>jSv`ru=-{KuuN%69yma zJpMy|YrB%hQpl5&&^Chkcga+i9Ys2L8BAAGMm9 zZTisQufW>n&Y$_M@Ql$4ILI7i?&_#O#f?sgjan>dM)hx zc`v)i@&t45O-zxU=C)gwS<PHgsxiE`1mTWiT9%Ae*!&P zb~yxPSoWHRp#I8ZG-?}zC{6!-1vhxF6x2L0gY9*2ut<}ia*uAFdkSWHPh&84%A8s? z9EG_#lr4pZP^~_3_$u?v>qJIk6e~3;n=-<)aFaN+dau>rvY;&5=6t@8q+G8(QClfb zW}(v<2<_kLy?(R%=U&sepv{vZu~n)k6MUxt01O&pfiO9@BYU(Q-8=kkVP+Td|M(#W zoWB3ez0wvarP{f9dz;L;R_Kw-woj#1^cQwJCpgtZ2W?h4H#YG-OVI`$bp8i|k!g%n zcliU>Bm+x;31hzE{Y12OT=C9SM9@92iUgN$;_~$UmE?9K#?raoDa0F2g!CgvkXMo4 zSQdFWP1wAiOIPIDS^g2eVAtAVQ16X-A#4#4`AFm00iQwNFqTu?o(yDN_u-VUCkT_7seuG-U{7IzldLpdcHv%Skm5WNfTs9y8HGN)- zfh8PU8I*bfPC-*W2=+!Z$FiZhn)#SOr{RL~OPY80KN2og9y!pOyd2=7 zp7}&Dl3{X$p%O{etZyA%rTMdQ(AIU;|LAJ7ADfuuqm%Rd7lR5XYcJo}htUXty)-pO zXeIb0;_km-@&?n2jw%nxC2GU!WY5kmy3!B(tQc%3A)<`q&IG)lTxv zqvf{8)&?}iJYM+7LdT0ugHl-D+85ml27uJ^?&E(Jq7nhih1evt(YGM>Ia5rr3OYkPzNst! zM>9I>*Hn=PX?OY2s-$7+kC}K8Dy)gz4fxe>=KmnKKkL~79R-X)U$vQQ zaW?+}ctunI@1igmGVRyg#a~<3+{LYgzz$`=jZ?Ir(TLoXoQmQSP}?Ilt`Uvti-Km% zSc_WLo_w!q7GH3Sz9x(-81OT#R=?N(r3_L94u9AetU1KHrGftYAoWrf6=*cbS3z?a zLZA|(-0Y%zX~@>4NR)nV;lo+%!<}<~ zUAxQ9?)DS-n7D6cMci&!>oyYrF8bUA6BGAo=W|4dlN4mCP+^}BpzPHLZgK5O*nrbE znB-sD%=Jv6)|Mr~=NQyFyVy~gjz^Q#3@C+t>d6%5pZ2d;PLU)g0jwlihZvE2;5TTz z!vA0@*&8H7c}pfn(~IbxZr@1*mcJdH#XokC7+|TDYa(*U61Wt^ZI<_ZbMZIX8KA=L z$SxSouc=5@9{ECKV`rJCPKZnz9Fw1wvEbsCPWwF6G%9fIMTqa@9Q<(BD{vIaSvhKe zw`kvand7Wkyl@kzk|oy9U0c`GuPI>2`Cj?P(XWkr7T)#82rzkW64gy8fI0n|He^pR6d|Bf|U9-^~=<1!_6}SAG~c;{bNeWrU35Q%_liYcOj3+eQ#RQ zg4u|c0`={8*qmP^V9^{Nu~Pr|ad;(41#pyQH)U0rT-kq0V*p@bVwXc6O6Rq)EFR6o z&`YY_^0v;<;+8XYm81`qD60`Uk&*wmvJRX8H#KdqLKTB(8P`oRt-!8kL)!g=CVgK1 zj`vElZ(<}~NkqN3%LB-&6^0y5Cs0}OrByi&VF3R}w_ZrnE}&BPvDEArNbl z4%16QD!KQf)2W1b#}D)8awqwIQBCE>PE(J~*vWZSu9evUA@Kk3^~ZeRHk^lz;>&C3 zgRHDxT)L=6IZ^!YFD=8khS#>d9N2=}|Ljgt=o#_95ZbkY3RE5+fWBs7Sg-FI^?wCY B;9~#) literal 4633 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 P00000NkvXXu0mjfY&D(f diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-orange.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-orange.png index 3dc60464cf31e3ba07d5585542d75d616f63da03..998f5fb3cb6f04dd8541efdc151bde4014aa69db 100755 GIT binary patch literal 26685 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 P00000NkvXXu0mjfY&D(f diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-pear-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-pear-overlay.png index 3dc60464cf31e3ba07d5585542d75d616f63da03..0d8d037a5f0357f90e13ff9cb05544cbd6d7d931 100755 GIT binary patch literal 26685 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 literal 4633 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 P00000NkvXXu0mjfY&D(f diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-pear.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-pear.png index 3dc60464cf31e3ba07d5585542d75d616f63da03..c8acaaa58349b47893c0c3ab3bbd4a2d472c8721 100755 GIT binary patch literal 26686 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 B?Uw)m literal 4633 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 P00000NkvXXu0mjfY&D(f diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircle@2x.png index 919d8f405ca7f40c921e71b91e7a81655db1f94b..207ce15dfabab74df8b5c13932506d94ef19dcfe 100755 GIT binary patch delta 95844 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�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 K?H<|qr~Dr=#f2~c delta 42344 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 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay-0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay-0@2x.png index 8e50cd033596fb3daacd173247d6779168cc8f99..ccd1494cdf1a9d19a9c860b88d240392ef1919ea 100755 GIT binary patch literal 87032 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 CAgN>k literal 26595 zcmXtf1z1z>`}f(#U?WF|#3&`CRX}QlGzv&5tssg>O2;`WPa6viJp%h001T&O{m*WzV4VU=C2sy1exeavrCgohQHSo z=)3a^$b`A};>N@aInm*~Svw3;Ns2#|j5!!(WMnw`uWvuM@GI>0wM#5;!LpX8=G0Mta&- zArpTakHV+)Uy6r`LhUyg(tpc(R(SQwRy&eEc+4o1mk%0V{V9?iz11t;{Z3+|Tjl_c z)N4C|uyz}Y;b<_%BI0cai|KC9-qp-yg%8v!LlK`}gl$n>B<9@H<|5Nbl{G%P$)mTa z846CJhJ%qk3)c06QoCq%O_AHT;DWF*@{wPX%Mb=VTaWSE5A+)`PIjgY9D zL$mv#-#RtnkoS;?@5_4NWo>;xsD9!cJaQsMTpU;b74So%S5?XX4#$+=%vEFNS5jSwxje`$OOrgV1Ag?N8;wNjk*7nUiiS7W^JcE@ zgi;0{mC!4SfXk|gDc8UGoZK8c34TlCoe2>uIrW>lc5>69_kZtygcG9uBxbF_OnQ#e zb-3uuErvR`b?xR}o|DPRmWqwYcd3YOJ)Gj4-0rTQdw3Sz)OJ8?k*1APr-G3f{jLSZ z4dT-(fYV%G?xhj#iZG`=ZBx{e&GpmqPuvOgBgOzYW*~m^20M$0pk3%b)j0}x zeeo3(#{fJDd6QVZ_~q1rx*&@a&{y{(csqoX+SwpII3W|8kD+t1ISdetZF4aUhXho& z%G*TgfKWgpaXD*SzKvjk)V|(SWel)&m2Ue2L@pO0$rNFLv26zdbo>(5@=n4QKTFKv zae)*(M`0Y|P@L$fr~gi3z=pY*yAT_j8|`TG5b25hD*~iJa%ll;pdj&pAsQhzw1 zUTnfnqPR^Vd<^EmGZcRuPe(MO4j6*qE&*TLP9ozZ#BQkcjJ{{U^m}jJbPzfG)vB`_ z7p-OV9|6&2v1!1Hr}=P}m`R6w286+Zmw?L+bx-KuE6#h$S6*~7G*I8+*wV ziTj1PCN!Q4aQLtDMM%jxLEWv*Dc}t2x!}Z;s43J|(pR_yEJ|wbW9S)a%NcXP8G^eE zd;&1K>T-`plm7dp9Qk6(M?kZ?OifS29O8YC`&dWV0Y+2>EzJ4>DlgO>i_e}GW{sVQ9(+VImrtJiLB@E{+8)jI_>DBdw+k_Lo7R`OJf~_dQ zu>7@U(f2Lvsu3#`<1!@LWT*^Lz6lUt790o8F9#Tlq|EnbcAIOj(FYE3R^=iA1Hcz; zIIkld^XTwY!X2g0XrV2FY1nA?$fq6hfSz8l)=H)!MHi)r&gj#xT!gIr2R-hD9I;taU8!lWr71 zc6sqyW+R;pMUfeO%5lNYtS(r3U@*4NnM>&U^%Jk@CyZ{kZ_jNO%vYKfNVJw&i%{nz9V=uDf^{@JEoAV+CHW!XU_3;ej9 zY^l$ipJv%1;VyeSS)qq#AxoiyXF01E+iM+d4m^=>nNjLM2qz==xtjUT^04b5glCFr zM3a9LbP@OnsvxS_5Y_w?jdzB#UJ!1$j~W%Ed8%!0c!>6gJREE$NGv*A9W6keVP%fh zj6gl~a27xM)80~D^Z3wZnW!v&_pYPkltxv2viD=gIE5ENhcobc%p(%TCSRC>-G;)pqV+b6SUQQ3cOo6k%faRC&-$)pUTT&Q{6h4V;O7*NTB&I!H; zH`paO9<${#CA8N$t?e^>CTN{f?!&VF^uAh^nxB`a8v7l!OM#9Fs=wyLj&Vx*s(VfQ zR?x&A7Mk0!3feU6?^&baNEfpo6@{5?!%SW)XB7gk_hzb=WXQeFj~GACZCrb+^e*S8 z=od->AMV-37oMqmdnUg051)R9;gzkYNPMf=XfD!ejU*%@s)DN3w(7)nAn1j@iep&# zPyNKT@N?H6{@1dMWdf)o2p9H-LXFz~{XE04Yo*thYM$73uUBaBvvf9EV8=Ep_Na$-tY-I+f{e)ALEzL+Ol!-U z%pbB|?+6@ftG}VI^=VGnTnH%RQ_|-RTz{m&d#z*Ng zXJmZz%t^-j@y!a)kzSM9kqKkJ%Bq`1>FLIcI)$mqJF4I*9^aiXFsD@%T8SCGbjk!! z=mEL*6OA0cTm>)fVb{V_&F4TEFn8_yD2pgC{-Z2rbdbslM6n4w3p7;C(mg#93HOi< z{L}VotaZzG_)joxdUmh(myy3&j@f&%6Wp!E10yxlo2L5vsbRUns_qZzLpcAkt+36p zO(h4oXyyi7IacDjhBVBR1+-2oGF^O(*?97JZ>7FRBk?ZL56yHxct6W}b#CNVd%=q} zo4l&m&h|H@FM2-i@prFvkhGx>mCjLm*gBl3?8`aW<aixs-)kQ%$;Rjxbhr(kxd8^+_^SpIm-+$6WQdvT}+uhS&AM3EKM4_AW#{6 zh(Ne27Ohb5e(oGi_Da`KphG$R&3wCyFN=F?^-C}trgzu%>I>2Bx&IEMnUxT42@3tC*mQ#VmLw6A$Mpb8_5f&JoH6crwI%9$u-zJ z4&GRo%o~0)7KFXPQ&l%Q{blXT{cJ--n1lb7hCpI z$(3;?)x^xZ*=J6-2UMDnudKF{~3DmObBRG#D|HRAuzq?09&qaeb9U(8g$?| zEySVawj2YIs;mvvi_8=5mkmnhP34!WRdp`Rl#gopIDbtUNGWB>q+5U9`lcYj))@1#<+9^s@m|0;mXDMWqckEUZ9V&uL%t=^h0T~Y z{?1(1y{E=+-sefS|hQjyJ*3CUQe+&}W z2-V__6Svc&VTq<0(xAgyeZQvj#=4s-0GtL3AC0!J{e}UvypIhQf(X<1t6rDHEX|Lc zd^uQONLrw{pWK={vp4-n$yIjGGMsaFW>PSN%;^N3VF|+Sd0e+d`0TZpP+X;mvm#bT zEt_PIB0>tvN|A^_LTiL!KD64c@!oTmr3Es!wx9Z6^#97MG#JV5*tg94Szw(sVmZ)r zgYVle3x>wZkg7zwLrWSxsXOF%zsoG~-c{HvEPJi>ZU@$nj0=9mPh3!u76zB4`d z_O_qZR@PZZeBOG$A41;R?${1%v?+R#f3c>u-27(rr4TS&=1Q4up7fB%A7y4uo#j6K zw^N=&?jy1fFo#X#ntk$Yd)7!T#WH>3$%EkI?2SJoe@14uX4*G93C=~lQ$n}mR{u-Y(@^d%H_JS0s^C*3Uwi_lf{)1?@;Gu_=Hj4G{g{q{%OF1Js zUYa*R2Fd{$^<$Wu&qT}_%SNE_>4mWutne*bmpKdzjoSAlKsz1X}PDdo1)rf zMeKArEUxlw&D=@5HibO%#K60F^mB^b6S~dJO|`-5oa#4T>Ng$?pV_l)vKqcPJ(L*4 z&7MuuFkf-**Xw81o+or6Ck*;P1QCCC?mh&Zf}EpH)e8k#C$i zU7ozCR`qSO3Y5=dyikfSqQ78@a_3+*MV(`G(65b`wB*F=3TW~iOoLAI@b~t6>d5}i zz^K~bnaSeGVo>|M_&yY}u2;dFc^V#lqn(nke z9>xr;!xELJxjFXlK~L30R-Vt5O(q1mWH zOSMJ3Aufuyfyr(A4GPP}+l-OGixr6&QhE}T~3EFOY^ z7d7Z0ld>`4oo7>OZ8m+u=M@wBEwDP*|ElXgJCbds^RGPGS#XYyVjzrX;f3lSVH+@z zSUiDd#&3%1h@;*o=|}qP1;`K7ng|5T^D)03^{_2tna!dcZ=~-uE;J5YS^h3KF3Kl% z;L}Akqlq9iq(I3c#7@Z3gKC%oMJUSO1!atg3Y@d0)h@y*upDaZ5mgNs1KJR}SxRk> z4)~U{s>5V7-z4ArI9esA#@)K+eV-I@QoN@h`Jgbhm;1uI`_+Fr?!F^6kj8%;C>Y}C z>>w`~PhIEe7~q>}fQ}B~O0#p?sV$5k0y+_@E{>`?mAvppL%DU?Ikc4%M3BMI)RL{P-*G8pHP2HdbS1)>wHsA%*b{ z60OO4Hw$(}_ME9M2fH4Rc9BeOvj1q{xQqHj`^KM#b>SKjTZip;!vn)BZ+=Q+nwIxg z#k_w6-DT=y{D~H6auNMib*L8T5mjKx^2=GG9lZ2k;aS4>`p-U5;(0>n z?J-ml-a2zQX)rqhx@YM)2jOdpL1#=bZ5NLm?rmDQ($O;Dgg6;#7ioiE(@h1^mP>4P zqRQ4kz8S7Bd`}JrqB{6T{pw@94{}3&DZMjtoz$_D#b4#2V>$bKl8aB)Vr?QTSX^$B z^M`{YHKLGzRYoXpqcvrC`y^sqtO0+^RErJ|D+nO3eU|SVYT=(n`w9>6#QA*)5O{o; z==}B1SkT$8TB~Y}u7eax_rdsvs3QC*(t{m+D*lDgV57gg-sbF6yX?6Cf+u4xPJ!#fid%>n&7V~K?g9cuSajaphxR5-F4{v z)!^RgcW(My$jW{~WVnjp)i>Unj%6=At$)Zrwtt5E3M)lJxEoaQuF2*@K5+9y|C%rA zn0irC=N{RwRg0kkpMjBCBrssIA_ zuIezd#iVk2YV)S zhpvY@??31J=zR?NHPCk3~ zq)Yp!kcO?9uk2C(^mjaO9t>35<-!U4l+&d$kIkzyfs^Da6~ED^*4WFx7itDtisZV{ zh`)a7lyv%2E4{g1gqURCAX`Wk<|w?c00OvtXA5zg3q;DQ^#w^j7=L2cxj#mJ6Ln45 z_L8mt9od}Ey12HB^^7x=w5K5|a{_8rcUdsz5-Ou?4Ghw`(`=_ki`cZ~^n_4#SlK95 z4k(BI@i+n9{bhcZ1kkT6W3eX(F@y$6?>Oa86hZl~8knH)kG?u47eYx;4A<|y)}kod z(-b!*4iWRo1OVr->QCTHbxtp&$Jjy3c-wwoG_r5x3TDZEvT${!El}ci@`DoZlBJRe z%|%V9szQxa>7wvdmc;Ol-8J0Y?JeDUT!yFpOU(7N2pT%8gIf-fAY5nq#B+Y|mSZ~2MLd?f`%^asQwI^Sq zuu^2sZlRB>9z<%a9!i(#PzG`rn1fyqfJjz`x4t={co7EE!WF&ByvsOu+P{k7+AAAG zOV>-wVRWb_ag`z^lri+4749Y5X_B3s(Jvnfr$4jA`WpuGm*%)%Q2!E~$cyB`GU|7A zbuIp`OF1-zh0CT@hi7#tCjavKq<_3%F`#*SL^N|z`Jref)4b~w$IG)l7=L6+ZDg4f zyufnoR7R|KUE7$*tF)JZoG<;;`HOF}rN22Z2|+BQITT4gD~$ijg@v=Q0R5jKGM)dZOP0uf;`Hv|q0UB<5(9#R1g6n%UVUQ7zjG!|Wi<@+OdjvU32viJ3^kb7Fn-)}32zYbeWPCzzam_0{} zW$VtdCWjZ%ik!zYY)rk1H+FAts`fAhZhs$cIK#V8GEy><^{O{1JJzV;w$vz@bBbT7 zX(h27EsgUohTdYuw9Ls3RLpE+;<)D?A8WE{8{Hx$ZOv@`vrT22Q5vT=-jk~#gx;yK zXId}{*%D^)5(8rzb^sf7{2=Epe|uf`f&KAax7(pG`w#B)fCi zwbcrI4H2IHQ4F%e6uv!!hc+{+&*)`cv!jW#e=c%U$eJ>K~3Sgp2l`5sXYN?<(A5k?m5 z6X9|!Ly0dlXuwcv2fRNz+<(2d{DR|;%KRQn$5BQNIIKxxvkA3n!}irIP{=>%sKzBQ|np^!aSc%)<%t{;9$=1 z)hws=ENf7VY#p3#CJz3qT*LlLH^a&kXDqg=>Tcl8PM5w>!Z- ztm=&>9o8A_mgnC>w*XozM)qGM}n-gcWxAb?%)R#Yu@yAG_O zhbsAa-Jg6trk!z?r2uIPW_>c9R-D9$jQ>2Dbl9%siWgA)kiwsi!6JY3A;hxklxO*0 z2%Ns&yY^3Dwo@9NWv`Z9O%M$?mCXO4tL7M{H2;zLsE;NJGBsKzfqNZGKsywl9>>x`fII*Ik-USOKZx1 zo$rWpFe>+yu(={W*9E22=NM0Z9?b)c0m5Np>VK&kwq7r{#d|ND4=B{}jK3q+4|~rU za$es5*B2*x4?SO8~S^`O?syx^NnA!cSU*`-z-^!}OI zn!NX`EIu;r+pEVK!n&Hghe~W2q7YY>JBEO8o46&amH#>;kLwfO=tnVj!9#)jPiBL9 zd;}6qF(y)JUNqr&3u`LrxVxK zgaZ7|iEci*^1~b@+;?sxwlZwJdo5;kJ*+UwavwmJV@ z%@mv!fKtwrdZU}sY`&4zhXGH|aL_6y%U20xzNvnlH@olbG}5~lpP2M}FG9K<6LqL1LYh2hQq!)U6jJvxsN z*5qONGk;`3U8MBxgw;Cr-KcfQH5;5G z7354yNv1g!LyZWwA;RsGVA}wlD!*`BX`Su&N16JE*q5CAO%Kn2 zjo}6hS6+mETy)YW%qLmQ)Or(G-Ap+Q6V`d$rbd>?^8!G!-h1Q3!t8OS>Sf0WLnRqHf>Hud^JmkK>n2 zWos5Q+7}OI^hPqSCt-$|Dj==_5Qb`o1>gwFJwlLbpxUP1`n7pyZu#BbG08e$^S9$i z&@ada!tA3gj}49}xk|@MBo8r^iy9dY`@Pq8Mf=rFGkuTJE1Q@6OGo+CLq|g+4~s&_ zo9>!rb;pVm27brR*+NaM@iGlSOwt|Lo$}IlAoBGpLH2hq?fbhY7OUUE68o&yq!t#G z;$zSfHoAIb>^lEG@rb$d60c>iWK5hD+=aIXgyKE|OC>;iT1vJ;{a=T%G8QN22j_j# zq}z=xL|@BZVidQ4SQ=qrGe#}zYUYptysp(DxAM_JIQ3i$)%Lk9c6`Uj>0kZM?_t#N zW(sX7O;pBi&Yy!$5}Eh#%c3n*JAr7L^KtY=@MAca;9ICfzQY@_-Q&5vD?4?e57esq zRc=dKVXR&LAg87TS>%Yf(&#Z#OMX48gn~_~LjmsnNKXB5esXMVes!i&7$UJ%pE>iHVMgdDoVh=)a;!0423_K8)xp*M0X6c~(R|rBx5=QV?+S7l zyk4QANLP6db!8yT2r8E(uACkBdw5o~{fk%;CCx&!sxxn)_j&PXLRs1C(f)5Cx0C|& zu34?h<%31kjJT+A<7afS0(9pNo*Ke?G7cyXu56nr=PuoMQa1QkhZS&sc_OEH}~T;f@Ke{ zzt>uhxU{$2EZTHqCB5Y9+gxM%DCs#xKPD*~q7dTWBI-jzeQ-nYx7Erz*}!c30$kbL zQc!3JewU?n%akXPy9;ZFCNJKnX_z*hj4ovJIbV`<S-bE1{$ zH8^>sk3Zuiq)~C}h44gn%9R^{;}Na0aOf=dCV4iysa_qdgZ}F?^t@MyAzb;cWj|i` zKoDJm`HM-l(Bxs7%G=4K)c*)huQrElpEtYVvPZ*(zQEXqY$eEFOl4bi`{$wS^}Q~RF$?mRby(JozIfL>{)ywMq^SLZsn$JKT>l|&o&9)bSn$=5B1mJ zOZi@e6UQsut56W@g)DUe2J%Ji4^B{0^V(2eZce3YPGi zVfWE#>2Q#N(+%Dq`k@c;6(5`AQF3Vfkg5>>bxMK}Q|Q#_*${)M7td_4Jre6o;pBt< zgVA&8A7kc2IEs%u1rGX)uvHs9^m>nxofeSRtuBpPb%6$-Cc*|&cx2CAK2CEsyGhiT zs6qwbT^JV=RA!sX7qZh+@2m74rFMEdYwM9x#o#3Odd!lmTIs<-HMxaH4F>+l3Hm

yftJ!Y0GsdcN%;MEM+5u>2KjHnOqx_AeVsl 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 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay-1@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay-1@2x.png index 3b5e886933d2f7a8803a44304c5439058ef86fa2..e611add3ba9861baf68614ec51d63d3f94c8592a 100755 GIT binary patch literal 119975 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

wSIutbvWi6D`m2u|!^@bLCI5us_1F3$vl-qM^r3 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≪}_ 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 E0Oy=DQvd(} literal 40858 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�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 U^GtX>4gf!RCwC#opET>M-<0j)>w_GlupaE zQdA~lAy}x?Kb);qief>Tio%>y85B1MMFn+;e-wn4iCWkYhE~K;Tv@diJE>EKHPvca zHDYSh5db zjM@LdW5GVBo-aEjj#cOe{8^>_9`zyW-9figw}yN!BrqBam}foI8>u%0|NK)VUQ17MwEp_u)0SWA_zYSk0`vKNWN>hh+`oUH)YsRO?c29Ulj?)i_fk)eOCKgtn&0myLqkIW z_}bc9vSrJbXh!!!VSvLU|LoZ_a_7z+Qc+PsX3m@$3HVRcrwIkD813_csCRU9kmJXX z6SLW@7y57N1;PP`8XfgXXJ;olapDA-KYzYn@G|O2LINv>5pQpACr6GPAx5K75444P zy0E}XA9uN2q^YTi6c-okf$pcCCoHgHUayxp91e2w=(y;pc(#hyQZ9_ZIAR;C&Y<=sIbDl{);&l`C3{ zkDby0E2H=M^XIin&>U%i(fa!O$gyL`w02LDr2)qJ8y6kBckkvw?~?`?ZDeGGSglsF zeED)-YJV&Zuu_;{3Cf9by;Q(Te?D~R5D)rOsep0!1ecZh`T4v7EQtjy+zEiBq$J45 z$bgcP64qC+A8(ZE$iu{m6Co!jhX=h+x-%YAr%r{98<~O3Wc6iXfya#-2ejmb($dn1 zqAc_&p2?FZLt!EF@f0o}mq`VT1K6B7b724e{j9HSmI@ec%9JU*DC54iNh)BB^4z&| zp}4qMt$9-T;3dULJdE~SF&`#6bcqDT&UKB`_>;zdC7y* zkL3i$s*7P6QUaSyCT49SB`_`~ScAB<7q!f0^VkArEEVMio<4oL>eM7HFz$k=jc3vV zvj#3{f$?h6m;!dY-D)2XOAFlF+pG5Rh_t}n-Q8**JGnR0L|-+jSf!K^W(`;vVJU$< z9uKRjM@nF))5&VGO9_lOVaH5_aSX=0&uVip^?OFyfW2NXoI7_;t*ISPF-jLd@8I^`A_qm>)_PVY}U~28-4p z-SQCCC>k3ZSe(1O}uxL-S@US3|hq84=j z@BTwh=%PZ%D*)krm$6Vl`;B^w+Hy)rL&XTZm&l3kvhZc2&=G$6^eLP=b&A#85{xiN z5n+_LI(_;yFS>|!> zv6w*7k%I>h>PcaodPYsI^}Pb4a1iQO*45SV#DhW|pK6cln*`nqup-Nf>)2hpcIlN| z{u$oEn*-h{umXy5Q03+2#9%P!0rkW(?ZgFGkxs|ax2&v;m`o-9RCLv5(>+ z-D0tjJ$v>5_HeJ99AZ~o4Byv$vQ7rFn!Knt-MUo*7rT`o9)C-<(qUi|^hXXo0 zJAneiwQJW#<2@~C_)@BJ>OTh0Tg2Vo#hZAr{;iKk>T0I+D1+j5Dax2lg9Nj;DIMeQ z!6UhNhOsetc;sH9Z|X-1jIR)C4S{S-lztfvF#f|spJV(5Z{CR44sVX$;?GXe55fE+ ZzyR4Xd;=*Q?iv69002ovPDHLkV1gL3>M{TT literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-roll-middle.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-roll-middle.png new file mode 100644 index 0000000000000000000000000000000000000000..03ebd1904875ab54fa71cbe948304fbb6266d69a GIT binary patch literal 140 zcmeAS@N?(olHy`uVBq!ia0vp^j6ht&!3HE(th_Z1NJ*BsMwA5Sr)zI%U~#c*aSiV>bgcqrenRo!(7sQ*l5b=5lS zHz>XqA5MduI3AJ3)o!d^br=$OWl&|sX@~VZ$$kjMf^*+5^|JIp^;7XGD@8l4m+Fq5 zbR@7C?o*Zte`1aIhsJvoa%oiq#8D}Ki%3xN-Ksnged2uiUDl?~c^n4)<4I1OL@J{s zVbM5~>zhOgK)+qf;w)+W21PgsudT$_W{dvh%J^~W25rSR8(jc< z_fL>Vf#GBu5+M8?a)ADqw~O-tr|7*i>Is87{CTFawH0p@a7Nez!b2*^x0zygxz-zJ zkY>w@5g3#`3)hxc7oS!>lk?q4A2}BKF4OE+l=$q;=o6RE{<#@Y^c6ReN2_0|6weK| z2uIAnbU>U^b5ZVub3{6?%JM{p_E&w1R}>YSKGQiTyB-Xk=tfFQe;f9 zT7frr1ow~=e}5}V$xMU*Y4I@*Evnes())7`go78n7;RKpIx`kD`E%XMaa-?OQZ-28 zi5A$#5ZvHTSuXzTXl?s@$h+&#_xArktA)i(04d2$ziJv9de^lR6uwRAw+mf3DxL5i zZ%jZ&vDSxmHFDjcMMePT4CJ&RQMrwF1c;wW1GqPo!cSZ^A(b-S3e=k6n)X>l7c;buJdiW4TJFtS@WP;S0XgZL zRZr~^B;3JzAm@^5`N8^|J9S$Eo}Uy8Bag4E`!ifO*#yn~@NB|Sx?P45|HI~V?QC?g z4jFo_Dw>(fI(BuPb+*GvzfZv!*mM-wnSs#~9FzwephKn{u!ofyos z#4hRx06epEi_y1?Qq$3HdB0QwjO&@~ShGCE8=pRb&BawoU$ZzWCoUoOIx7L1t28)d z`yS1UG#((0hLy3j40^L2$5CnL#>$kX73~K-M7th}=wN8AyWKeS(#q%ETnn=#%<90t ze7vB1Kb>x9o>VALdA9*Ak}r2xz8Rw57nC4M3H<_h<7Ki1Fz`+PZbuO%l5Br0a(oSA zg;^8KgM`6aeM%%kx-bcr>ZGgP7Qr&44d-K|2JBpyh%9M!COO<|4q{>(ULo9=0@1|` z{mdIm##t*?42H{S5)ek>iXW($sYURj-j(3}Ao(XED3+A5XEmxS;3@V8-Lioga8`Zm zKGNssuY$f>>S4+Fubmb-0Ub}(`9&iCk5&1fTkn{v_16n9O~0IyKJrn1yh6BoSU@*>AiDNP`V(J;9{Ld+x^0JNiTBTS>lAaos86NG z4tj6a1y0egJwb*di9UsPQkh zVUV*XtU<4yq=oM3s=AeRG6WkrM2iyo(WeWwm?<>=Qy7 zPz!0(bK?q3h@tu4v->WnEttwXV40o7A14u+VB^TJY0coQo|V0F9X(&cElEqk8XNmG iXXL~5fUGV&%!C*_ol6Q};|lnZLeiKIqOXgS)&B#53)X=E literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-drum-outer.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-drum-outer.png new file mode 100644 index 0000000000000000000000000000000000000000..87ead80a08146adfa553d242f315f764a467506e GIT binary patch literal 3749 zcmXw6d0Z3swx1=LBqV_l2tpJXHVFc<7z6}lk|44O=CUc^0?0D5i>PQHn1sckh@oT!C;JI*I`XIcAccz ztC}`q&oYk*p3hkX13op$zXB}GMZBEC3J1CjSO)|jdz90TtU2KMYwR;q-XnbZTI^fD zLJZ62jytf|MAtpnf2D> z#6IJM5^1M9pS(g4{WuXVp)xz)VxJc^d>R(Q6R*S!cL|UpcyhT8M?dg!G?o-ndJoxRI(mVU23OpxhqLoi@56knwDI0VpbMWZRnyBBD1N#AxL~phh@4F3Cfi4TyLM-|>4|JHW*`<2$ zHh_NxiAVzHF;t>kY|-ue+~|UAchYNQJtntZ=Bym-Pzn@~^(e^Kha!dmAR~ z?AX^b>8E?q@4e^T2nfk*7x(UMj(lx|U-uLYCY2)QJaMET+O(CNN6`dhE|iGM<_i%N zZju>xi6K9Q?x-xee7;-Ot7|Vt_NamruQ>GFhj{J%blHG8LKcWSJd-E`?GY&`orWw`7{bCr1Y;0Z48o zEo-`v6mjsB?wW1;XHVq07=LgzxOum7oXhO6Hv^^|@XRm46B3Au?m^gRdp{wAOKvKi z2IzW=I$PCRYLpvEb4NPuq6Wk6L6)clu~q0OuZQi(L)-D=-c}74?#thma@3a^NI5cp zSxekWk|_o-Q&F2E@lRAI=ylA^SmN*|UGNf*QPbA`4F(z$<>!5O4R{iO%aCd${0(nu z3EI6Ch*>KZDH*EJ@E2g@Jv7z_!N(ZdywVyeX+RdJWUgA@W>rhRqx|BV@`!Gr6XMFb z)dsw7`gTGmn*h4|Y!+}meOa`yil&S6qZog>r;I4e#o-Sg4rie8x3g?ln`7m>k*KS0*j(HmBpi%ON%Ynq zb!J|?S1#qqdLQGC!r(D%cp+Z*g*H)kn6Ia?Fp-Ty@h~P&RowGWh63!%x9zr~t9w3} zo_PN`DC{7GP;gKrJzb#Qns3P!H|gG+s;)Fq^s6s1b2jyMyZ&R;1Oq%g7y*)||Hk6; zjA|ZG_?Zn<%R3O$?=(BhhKS%QCi61JMz6=>f4n!XIU!siw^RO}a7pvY8kJOJqJ6)% zuG0>it&(;qw;~8;<%gYRh`P1#1WvtngvF9A51wjSOt6TQYJi9UWi}-tyNlUyfeMNct$8i$=dV_-iljkE*QrfL zwA#fUCusc7vdf)jhVx#?Y!@$Sddj4*k)&x^Ode~6u6=SQ@Tv*CiI+WPJJ-p#Rvv)6 zT;bH^7F&`jL?VB~K2aBTodBwaq!9%;V(I26&AIoK$HtI$}y zA(4RX=$G=Amesn;mNr0`;5|dJdu6Q)QP6&01@_cWEj@3o-(f?-vW)F@S-ynpNhv8T z!rGkGSV0B1;^#H{nnvfDQrOwGKyAZPi%}OWQqL8mL+EBzt zMvk{}^wMp8(7f{~^h-rc%dVi-(ek0(rx;P@8q<)@@(-HpgWH4;L!Z`!5p=dn-yPQ0 zYOYDtAp4t5d0n`sHnj7Hb^mWNr)Yr6fb0X@Z&b;*H`~tSRq0|#Rw~k0%sC5I;&O92 zgO`tjBwdJsef{zHP(}Gpg5q{zUWs6Nnpq|v|D&(kJ>~zb{tBt5p6M*@>OI^*MEgBd$q4sx<}5 zT==*&-S-=7n!UL9jR>w}0Ua3^;E2$tdM1&NFjSi9r4v%gwgW$%V}v|>L6pUw#9qd4 zlbQ)x+eavWIVB|#v&$Mg`!?yxV!G{2jxs0ZwS}VP=(kp}Q^&!_Yy)iXmbNtB5W=*T z&Ct9y+9uVnDiE)s`ln%w4kDXfh_7mYfWk+0LTjv7*po2F;{TDD>GS8I`~5jnf@pUkb-#;%|oa)Aox|M?`B|qK- z&Qo^bOkwnSImb5I7=JP;!cSShdH@kB`5O(NQgqkG73s>$07#CGlP z#9mUFhh*!r$tVAVQAiivS-WQK#|oQ1G8D^>VC7!7GKydl>kf;7ieruRxg5hBKKsr| zBz7uGHDUYxp(qj+t+RBT9$65bS?Z*~a-UTg?!w&_ zJG=b~%-|o!ORA*%9WN5W51>xpVJ9f+-Y~0#Sw+EWpxq3%j1E5R2NAb}C+>0CswUZ4!N9n5VcTB;5-ShRi!N^@9+{59#CEI|-_0}jH>*RJ5VMH_r@)eWYP zJCOMyJ4xBk^dXWZJnp%V5SR2Ubotvq)m%CAm|$0K5KsHkg4rN{J~E}kKdPAh z;6*3m3%Wk!y72yF)5sR(3h=xnL7CE{B&LGO(zN(UM0$S($5tDa!=5$jT$#An1V2^w(T z)FV87GZuHYFmpdu6E%2B>Y}J517KN+x3>dO%N_o@dJ;_g>4+_hGGs3#R$ zRP)JriQ!Ly0@%=u*YonRXA6f=G;5x{6)|?d+FX6MbmN*?6YlyaD)2ogtRI3~CO?RI zo3$WOEm_P_|9yzrLN%Fr@%`-AAHjXuLpBtEWDbnH%{^(n#*jn&X9A(CB7=SwCdvL6 DZJ4!X literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-roll-end.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-roll-end.png new file mode 100644 index 0000000000000000000000000000000000000000..778c231f20ca9253e64ccffb64a0aa39a3b97a76 GIT binary patch literal 14570 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 PK2D*pzfZY$bjE)HM$sIi literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikobigcircle@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikobigcircle.png similarity index 100% rename from osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikobigcircle@2x.png rename to osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikobigcircle.png diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikobigcircleoverlay.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikobigcircleoverlay.png new file mode 100644 index 0000000000000000000000000000000000000000..6dca33317131cd69ef449c0a6c0122f0f39cd84c GIT binary patch literal 66280 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 e__0p9W$exLq4HUNEC{AIAxiS~PfR=%CLeS{yi$ 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 Qd;A||`ImC#G8P~IA7%MrKL7v# literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikohitcircleoverlay.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikohitcircleoverlay.png new file mode 100644 index 0000000000000000000000000000000000000000..2626c394dcdccdb7d74da5460ec3f6d6570ad9a5 GIT binary patch literal 66281 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 z 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�kcUrvy?Ky7PM-eKS~7g% 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 literal 0 HcmV?d00001 From 6674c702800ad0d876808e06ade20e43f2b694e8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 3 Sep 2023 01:20:01 +0300 Subject: [PATCH 1246/2100] Add support for limiting skin texture/animation dimensions --- osu.Game/Skinning/IAnimationTimeReference.cs | 3 +- osu.Game/Skinning/LegacySkinExtensions.cs | 35 ++++++++++++++----- .../Drawables/DrawableStoryboardAnimation.cs | 2 +- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/osu.Game/Skinning/IAnimationTimeReference.cs b/osu.Game/Skinning/IAnimationTimeReference.cs index b6a944ddf8..91f1171a72 100644 --- a/osu.Game/Skinning/IAnimationTimeReference.cs +++ b/osu.Game/Skinning/IAnimationTimeReference.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Textures; using osu.Framework.Timing; +using osuTK; namespace osu.Game.Skinning { @@ -13,7 +14,7 @@ namespace osu.Game.Skinning ///

KVsm2B>cb8^}lrej~MtL3IA_&{r^T6&VRN% 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

/// /// This should not be used to start an animation immediately at the current time. - /// To do so, use with startAtCurrentTime = true instead. + /// To do so, use with startAtCurrentTime = true instead. /// [Cached] public interface IAnimationTimeReference diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 0d2461567f..ad3b10edd3 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osuTK; using static osu.Game.Skinning.SkinConfiguration; namespace osu.Game.Skinning @@ -18,16 +19,16 @@ namespace osu.Game.Skinning public static partial class LegacySkinExtensions { public static Drawable? GetAnimation(this ISkin? source, string componentName, bool animatable, bool looping, bool applyConfigFrameRate = false, string animationSeparator = "-", - bool startAtCurrentTime = true, double? frameLength = null) - => source.GetAnimation(componentName, default, default, animatable, looping, applyConfigFrameRate, animationSeparator, startAtCurrentTime, frameLength); + bool startAtCurrentTime = true, double? frameLength = null, Vector2? maxSize = null) + => source.GetAnimation(componentName, default, default, animatable, looping, applyConfigFrameRate, animationSeparator, startAtCurrentTime, frameLength, maxSize); public static Drawable? GetAnimation(this ISkin? source, string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool animatable, bool looping, bool applyConfigFrameRate = false, - string animationSeparator = "-", bool startAtCurrentTime = true, double? frameLength = null) + string animationSeparator = "-", bool startAtCurrentTime = true, double? frameLength = null, Vector2? maxSize = null) { if (source == null) return null; - var textures = GetTextures(source, componentName, wrapModeS, wrapModeT, animatable, animationSeparator, out var retrievalSource); + var textures = GetTextures(source, componentName, wrapModeS, wrapModeT, animatable, animationSeparator, maxSize, out var retrievalSource); switch (textures.Length) { @@ -53,7 +54,7 @@ namespace osu.Game.Skinning } } - public static Texture[] GetTextures(this ISkin? source, string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool animatable, string animationSeparator, out ISkin? retrievalSource) + public static Texture[] GetTextures(this ISkin? source, string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool animatable, string animationSeparator, Vector2? maxSize, out ISkin? retrievalSource) { retrievalSource = null; @@ -78,7 +79,9 @@ namespace osu.Game.Skinning } // if an animation was not allowed or not found, fall back to a sprite retrieval. - var singleTexture = retrievalSource.GetTexture(componentName, wrapModeS, wrapModeT); + var singleTexture = maxSize != null + ? retrievalSource.GetTextureWithMaxSize(componentName, maxSize.Value, wrapModeS, wrapModeT) + : retrievalSource.GetTexture(componentName, wrapModeS, wrapModeT); return singleTexture != null ? new[] { singleTexture } @@ -88,9 +91,11 @@ namespace osu.Game.Skinning { for (int i = 0; true; i++) { - Texture? texture; + var texture = maxSize != null + ? skin.GetTextureWithMaxSize(getFrameName(i), maxSize.Value, wrapModeS, wrapModeT) + : skin.GetTexture(getFrameName(i), wrapModeS, wrapModeT); - if ((texture = skin.GetTexture(getFrameName(i), wrapModeS, wrapModeT)) == null) + if (texture == null) break; yield return texture; @@ -100,6 +105,20 @@ namespace osu.Game.Skinning string getFrameName(int frameIndex) => $"{componentName}{animationSeparator}{frameIndex}"; } + public static Texture? GetTextureWithMaxSize(this ISkin source, string componentName, Vector2 maxSize, WrapMode wrapModeS = WrapMode.None, WrapMode wrapModeT = WrapMode.None) + { + var texture = source.GetTexture(componentName, wrapModeS, wrapModeT); + if (texture == null) + return texture; + + if (texture.DisplayWidth <= maxSize.X && texture.DisplayHeight <= maxSize.Y) + return texture; + + // use scale adjust property for downscaling the texture in order to meet the specified maximum dimensions. + texture.ScaleAdjust *= Math.Max(texture.DisplayWidth / maxSize.X, texture.DisplayHeight / maxSize.Y); + return texture; + } + public static bool HasFont(this ISkin source, LegacyFont font) { return source.GetTexture($"{source.GetFontPrefix(font)}-0") != null; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 82c01ea6a1..309ca63896 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -137,7 +137,7 @@ namespace osu.Game.Storyboards.Drawables // When reading from a skin, we match stables weird behaviour where `FrameCount` is ignored // and resources are retrieved until the end of the animation. - foreach (var texture in skin.GetTextures(Path.GetFileNameWithoutExtension(Animation.Path)!, default, default, true, string.Empty, out _)) + foreach (var texture in skin.GetTextures(Path.GetFileNameWithoutExtension(Animation.Path)!, default, default, true, string.Empty, null, out _)) AddFrame(texture, Animation.FrameDelay); } From 351081eb278ff0f48fff992ba9f0e0de2cf98814 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 3 Sep 2023 01:20:40 +0300 Subject: [PATCH 1247/2100] Add limit to osu! hit circle elements --- .../Skinning/Legacy/LegacyMainCirclePiece.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index cadac4d319..45a18152c2 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public partial class LegacyMainCirclePiece : CompositeDrawable { + private static readonly Vector2 circle_piece_size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + public override bool RemoveCompletedTransforms => false; /// @@ -51,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy this.priorityLookupPrefix = priorityLookupPrefix; this.hasNumber = hasNumber; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = circle_piece_size; } [BackgroundDependencyLoader] @@ -68,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png. InternalChildren = new[] { - CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) }) + CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTextureWithMaxSize(circleName, circle_piece_size) }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -77,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d)) + Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d, maxSize: circle_piece_size)) { Anchor = Anchor.Centre, Origin = Anchor.Centre, From d286816ba8b1beefe81457dd2533ecc9c937b449 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 3 Sep 2023 01:21:02 +0300 Subject: [PATCH 1248/2100] Add limit to taiko hit elements --- .../Skinning/Legacy/LegacyCirclePiece.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs index 5516e025cd..c94016d2b1 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public partial class LegacyCirclePiece : CompositeDrawable, IHasAccentColour { + private static readonly Vector2 circle_piece_size = new Vector2(128); + private Drawable backgroundLayer = null!; private Drawable? foregroundLayer; @@ -52,9 +54,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy string prefix = ((drawableHitObject.HitObject as TaikoStrongableHitObject)?.IsStrong ?? false) ? big_hit : normal_hit; - return skin.GetAnimation($"{prefix}{lookup}", true, false) ?? + return skin.GetAnimation($"{prefix}{lookup}", true, false, maxSize: circle_piece_size) ?? // fallback to regular size if "big" version doesn't exist. - skin.GetAnimation($"{normal_hit}{lookup}", true, false); + skin.GetAnimation($"{normal_hit}{lookup}", true, false, maxSize: circle_piece_size); } // backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer. @@ -96,7 +98,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy // Not all skins (including the default osu-stable) have similar sizes for "hitcircle" and "hitcircleoverlay". // This ensures they are scaled relative to each other but also match the expected DrawableHit size. foreach (var c in InternalChildren) - c.Scale = new Vector2(DrawHeight / 128); + c.Scale = new Vector2(DrawHeight / circle_piece_size.Y); if (foregroundLayer is IFramedAnimation animatableForegroundLayer) animateForegroundLayer(animatableForegroundLayer); From f182f571cbaa019ed3ad3272ff208dea98ac51a0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 3 Sep 2023 02:22:41 +0300 Subject: [PATCH 1249/2100] Add limit to catch palpable object elements --- .../Skinning/Legacy/LegacyBananaPiece.cs | 8 ++++++-- .../Skinning/Legacy/LegacyDropletPiece.cs | 7 +++++-- .../Skinning/Legacy/LegacyFruitPiece.cs | 12 ++++++++---- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyBananaPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyBananaPiece.cs index 26832b7271..9f99e3a586 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyBananaPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyBananaPiece.cs @@ -2,17 +2,21 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Textures; +using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Catch.Skinning.Legacy { public partial class LegacyBananaPiece : LegacyCatchHitObjectPiece { + private static readonly Vector2 banana_max_size = new Vector2(128); + protected override void LoadComplete() { base.LoadComplete(); - Texture? texture = Skin.GetTexture("fruit-bananas"); - Texture? overlayTexture = Skin.GetTexture("fruit-bananas-overlay"); + Texture? texture = Skin.GetTextureWithMaxSize("fruit-bananas", banana_max_size); + Texture? overlayTexture = Skin.GetTextureWithMaxSize("fruit-bananas-overlay", banana_max_size); SetTexture(texture, overlayTexture); } diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs index 7ffd682698..63be1bcf91 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs @@ -2,12 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Textures; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Catch.Skinning.Legacy { public partial class LegacyDropletPiece : LegacyCatchHitObjectPiece { + private static readonly Vector2 droplet_max_size = new Vector2(100); + public LegacyDropletPiece() { Scale = new Vector2(0.8f); @@ -17,8 +20,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy { base.LoadComplete(); - Texture? texture = Skin.GetTexture("fruit-drop"); - Texture? overlayTexture = Skin.GetTexture("fruit-drop-overlay"); + Texture? texture = Skin.GetTextureWithMaxSize("fruit-drop", droplet_max_size); + Texture? overlayTexture = Skin.GetTextureWithMaxSize("fruit-drop-overlay", droplet_max_size); SetTexture(texture, overlayTexture); } diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs index 85b60561dd..e4d25e036b 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs @@ -2,11 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Catch.Skinning.Legacy { internal partial class LegacyFruitPiece : LegacyCatchHitObjectPiece { + private static readonly Vector2 fruit_max_size = new Vector2(128); + protected override void LoadComplete() { base.LoadComplete(); @@ -22,19 +26,19 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy switch (visualRepresentation) { case FruitVisualRepresentation.Pear: - SetTexture(Skin.GetTexture("fruit-pear"), Skin.GetTexture("fruit-pear-overlay")); + SetTexture(Skin.GetTextureWithMaxSize("fruit-pear", fruit_max_size), Skin.GetTextureWithMaxSize("fruit-pear-overlay", fruit_max_size)); break; case FruitVisualRepresentation.Grape: - SetTexture(Skin.GetTexture("fruit-grapes"), Skin.GetTexture("fruit-grapes-overlay")); + SetTexture(Skin.GetTextureWithMaxSize("fruit-grapes", fruit_max_size), Skin.GetTextureWithMaxSize("fruit-grapes-overlay", fruit_max_size)); break; case FruitVisualRepresentation.Pineapple: - SetTexture(Skin.GetTexture("fruit-apple"), Skin.GetTexture("fruit-apple-overlay")); + SetTexture(Skin.GetTextureWithMaxSize("fruit-apple", fruit_max_size), Skin.GetTextureWithMaxSize("fruit-apple-overlay", fruit_max_size)); break; case FruitVisualRepresentation.Raspberry: - SetTexture(Skin.GetTexture("fruit-orange"), Skin.GetTexture("fruit-orange-overlay")); + SetTexture(Skin.GetTextureWithMaxSize("fruit-orange", fruit_max_size), Skin.GetTextureWithMaxSize("fruit-orange-overlay", fruit_max_size)); break; } } From d674856e29d230fb6efd6b5a9c0873f6db628050 Mon Sep 17 00:00:00 2001 From: Magnus-Cosmos Date: Sat, 2 Sep 2023 22:49:29 -0400 Subject: [PATCH 1250/2100] Use existing localisations in `BeatmapInfoWedge` --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 81759f6787..8bbf569566 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -30,6 +30,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; using osu.Game.Graphics.Containers; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Screens.Select { @@ -371,7 +372,7 @@ namespace osu.Game.Screens.Select { new InfoLabel(new BeatmapStatistic { - Name = $"Length (Drain: {playableBeatmap.CalculateDrainLength().ToFormattedDuration().ToString()})", + Name = BeatmapsetsStrings.ShowStatsTotalLength(playableBeatmap.CalculateDrainLength().ToFormattedDuration()), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length), Content = working.BeatmapInfo.Length.ToFormattedDuration().ToString(), }), @@ -415,7 +416,7 @@ namespace osu.Game.Screens.Select bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic { - Name = "BPM", + Name = BeatmapsetsStrings.ShowStatsBpm, CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm), Content = labelText }); From 40dbf098d2d31730ddecf38032da48a8d2461910 Mon Sep 17 00:00:00 2001 From: Magnus-Cosmos Date: Sat, 2 Sep 2023 22:51:08 -0400 Subject: [PATCH 1251/2100] Use existing localisation for "view profile" --- osu.Game/Graphics/Containers/LinkFlowContainer.cs | 3 ++- osu.Game/Users/Drawables/ClickableAvatar.cs | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index 2d27ce906b..40e883f8ac 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -15,6 +15,7 @@ using osu.Framework.Localisation; using osu.Framework.Platform; using osu.Game.Online; using osu.Game.Users; +using osu.Game.Localisation; namespace osu.Game.Graphics.Containers { @@ -74,7 +75,7 @@ namespace osu.Game.Graphics.Containers } public void AddUserLink(IUser user, Action creationParameters = null) - => createLink(CreateChunkFor(user.Username, true, CreateSpriteText, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user), "view profile"); + => createLink(CreateChunkFor(user.Username, true, CreateSpriteText, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user), ContextMenuStrings.ViewProfile); private void createLink(ITextPart textPart, LinkDetails link, LocalisableString tooltipText, Action action = null) { diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index e74ffc9d54..677a8fff36 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -6,14 +6,13 @@ using osu.Framework.Allocation; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.Containers; +using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Users.Drawables { public partial class ClickableAvatar : OsuClickableContainer { - private const string default_tooltip_text = "view profile"; - public override LocalisableString TooltipText { get @@ -21,7 +20,7 @@ namespace osu.Game.Users.Drawables if (!Enabled.Value) return string.Empty; - return ShowUsernameTooltip ? (user?.Username ?? string.Empty) : default_tooltip_text; + return ShowUsernameTooltip ? (user?.Username ?? string.Empty) : ContextMenuStrings.ViewProfile; } set => throw new NotSupportedException(); } From ae9c901b94201de270a3c2fd6ccdbed59107da05 Mon Sep 17 00:00:00 2001 From: Magnus-Cosmos Date: Sun, 3 Sep 2023 01:45:22 -0400 Subject: [PATCH 1252/2100] Fix `BeatmapInfoWedge` tests failing due to BPM --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index a470ed47d4..7cd4f06bce 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -15,6 +15,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; @@ -188,7 +189,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddUntilStep($"displayed bpm is {target}", () => { - var label = infoWedge.DisplayedContent.ChildrenOfType().Single(l => l.Statistic.Name == "BPM"); + var label = infoWedge.DisplayedContent.ChildrenOfType().Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsBpm); return label.Statistic.Content == target; }); } From 079792644886a6c57fb03eee397d9594419e5847 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sun, 3 Sep 2023 12:19:03 +0300 Subject: [PATCH 1253/2100] Update VerticalAttributeDisplay.cs --- osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs b/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs index 2ad420657c..95d979ebd2 100644 --- a/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs +++ b/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs @@ -12,7 +12,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osuTK; - namespace osu.Game.Overlays.Mods { public partial class VerticalAttributeDisplay : Container, IHasCurrentValue From 8281ed5173af43556f8b2e5ba937a2e66eff7f3a Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sun, 3 Sep 2023 14:51:53 +0300 Subject: [PATCH 1254/2100] Fixed "no animations" issue --- osu.Game/Overlays/Mods/ModMapInfoContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Mods/ModMapInfoContainer.cs b/osu.Game/Overlays/Mods/ModMapInfoContainer.cs index 378e6f6057..281fe8abe5 100644 --- a/osu.Game/Overlays/Mods/ModMapInfoContainer.cs +++ b/osu.Game/Overlays/Mods/ModMapInfoContainer.cs @@ -50,6 +50,7 @@ namespace osu.Game.Overlays.Mods const float corner_radius = 7; const float border_thickness = 2; + AutoSizeAxes = Axes.Both; InternalChild = content = new InputBlockingContainer { Origin = Anchor.BottomRight, From b17a55d6a84aba14c6387a03d64053f5b82d4751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nguy=C3=AAn=20Minh?= Date: Mon, 4 Sep 2023 10:43:05 +0700 Subject: [PATCH 1255/2100] Add length check for slider velocity --- .../Edit/Compose/Components/Timeline/DifficultyPointPiece.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs index 173a665d5c..366518eb58 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs @@ -177,6 +177,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline AddHeader("Final velocity"); AddValue($"{beatmapVelocity * current.Value:#,0.00}x"); + if (sliderVelocities.Length == 0) return; if (sliderVelocities.First() != sliderVelocities.Last()) { AddHeader("Beatmap velocity range"); From d5a89c4c45eddff8eb2d88cb7739c08e3260fecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nguy=C3=AAn=20Minh?= Date: Mon, 4 Sep 2023 13:32:42 +0700 Subject: [PATCH 1256/2100] Fix formatting --- .../Compose/Components/Timeline/DifficultyPointPiece.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs index 366518eb58..99fb2ab874 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs @@ -177,7 +177,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline AddHeader("Final velocity"); AddValue($"{beatmapVelocity * current.Value:#,0.00}x"); - if (sliderVelocities.Length == 0) return; + if (sliderVelocities.Length == 0) + { + return; + } + if (sliderVelocities.First() != sliderVelocities.Last()) { AddHeader("Beatmap velocity range"); From 0a1ba2ebe08877717f4617cb4d3070ea33d3b3b9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 4 Sep 2023 15:56:32 +0900 Subject: [PATCH 1257/2100] Remove ModNoMod usage --- .../Mods/TestSceneManiaModDoubleTime.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs index 08e83b04b5..00b79529a9 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs @@ -7,7 +7,6 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Replays; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Replays; using osu.Game.Tests.Visual; @@ -23,7 +22,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods [Test] public void TestHitWindowWithoutDoubleTime() => CreateModTest(new ModTestData { - Mod = new ModNoMod(), PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value != 1, Autoplay = false, Beatmap = new Beatmap From 58844092d6984f96e5b7f1fd263d675979c8b879 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Mon, 4 Sep 2023 16:17:21 +0900 Subject: [PATCH 1258/2100] post a notification instead a screen --- .../Online/TestSceneReplayMissingBeatmap.cs | 14 +- .../Database/MissingBeatmapNotification.cs | 157 ++++++++++++++ osu.Game/OsuGame.cs | 2 - osu.Game/Scoring/ScoreImporter.cs | 10 +- osu.Game/Scoring/ScoreManager.cs | 7 - .../Import/ReplayMissingBeatmapScreen.cs | 199 ------------------ 6 files changed, 169 insertions(+), 220 deletions(-) create mode 100644 osu.Game/Database/MissingBeatmapNotification.cs delete mode 100644 osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs b/osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs index eb84d80051..60197e0eb7 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs @@ -1,13 +1,15 @@ // 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.Net; using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Screens.Import; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.Online @@ -24,6 +26,12 @@ namespace osu.Game.Tests.Visual.Online OnlineBeatmapSetID = 173612, BeatmapSet = new APIBeatmapSet { + Title = "FREEDOM Dive", + Artist = "xi", + Covers = new BeatmapSetOnlineCovers + { + Card = "https://assets.ppy.sh/beatmaps/173612/covers/card@2x.jpg" + }, OnlineID = 173612 } }; @@ -40,7 +48,7 @@ namespace osu.Game.Tests.Visual.Online } }); - AddUntilStep("Replay missing screen show", () => Game.ScreenStack.CurrentScreen.GetType() == typeof(ReplayMissingBeatmapScreen)); + AddUntilStep("Replay missing notification show", () => Game.Notifications.ChildrenOfType().Any()); } [Test] @@ -58,7 +66,7 @@ namespace osu.Game.Tests.Visual.Online } }); - AddUntilStep("Replay missing screen not show", () => Game.ScreenStack.CurrentScreen.GetType() != typeof(ReplayMissingBeatmapScreen)); + AddUntilStep("Replay missing notification not show", () => !Game.Notifications.ChildrenOfType().Any()); } private void setupBeatmapResponse(APIBeatmap b) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs new file mode 100644 index 0000000000..2587160a57 --- /dev/null +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -0,0 +1,157 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Scoring; +using osuTK.Graphics; + +namespace osu.Game.Database +{ + public partial class MissingBeatmapNotification : ProgressNotification + { + [Resolved] + private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; + + [Resolved] + private ScoreManager scoreManager { get; set; } = null!; + + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + [Resolved] + private BeatmapSetOverlay? beatmapSetOverlay { get; set; } + + private Container beatmapPanelContainer = null!; + + private readonly MemoryStream scoreStream; + + private readonly APIBeatmapSet beatmapSetInfo; + + private BeatmapDownloadTracker? downloadTracker; + + private Bindable autodownloadConfig = null!; + + public MissingBeatmapNotification(APIBeatmap beatmap, MemoryStream scoreStream) + { + beatmapSetInfo = beatmap.BeatmapSet!; + + this.scoreStream = scoreStream; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, OsuConfigManager config) + { + autodownloadConfig = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating); + + Text = "You do not have the required beatmap for this replay"; + + Content.Add(beatmapPanelContainer = new ClickableContainer + { + RelativeSizeAxes = Axes.X, + Height = 70, + Anchor = Anchor.CentreLeft, + Origin = Anchor.TopLeft, + Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapSetInfo.OnlineID) + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + downloadTracker = new BeatmapDownloadTracker(beatmapSetInfo); + downloadTracker.State.BindValueChanged(downloadStatusChanged, true); + + beatmapPanelContainer.Clear(); + beatmapPanelContainer.Child = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 4, + Children = new Drawable[] + { + downloadTracker, + new DelayedLoadWrapper(() => new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.Card) + { + OnlineInfo = beatmapSetInfo, + RelativeSizeAxes = Axes.Both + }) + { + RelativeSizeAxes = Axes.Both + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.4f + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding + { + Left = 10f, + Top = 5f + }, + Children = new Drawable[] + { + new TruncatingSpriteText + { + Text = beatmapSetInfo.Title, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), + RelativeSizeAxes = Axes.X, + }, + new TruncatingSpriteText + { + Text = beatmapSetInfo.Artist, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 12, italics: true), + RelativeSizeAxes = Axes.X, + } + } + }, + new DownloadButton + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Width = 50, + Height = 30, + Margin = new MarginPadding + { + Bottom = 1f + }, + Action = () => beatmapDownloader.Download(beatmapSetInfo), + State = { BindTarget = downloadTracker.State } + } + } + }; + + if (autodownloadConfig.Value) + beatmapDownloader.Download(beatmapSetInfo); + } + + private void downloadStatusChanged(ValueChangedEvent status) + { + if (status.NewValue != DownloadState.LocallyAvailable) + return; + + var importTask = new ImportTask(scoreStream, "score.osr"); + scoreManager.Import(this, new[] { importTask }); + } + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5d130af6d4..c60bff9e4c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -854,8 +854,6 @@ namespace osu.Game MultiplayerClient.PostNotification = n => Notifications.Post(n); - ScoreManager.Performer = this; - // make config aware of how to lookup skins for on-screen display purposes. // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. LocalConfig.LookupSkinName = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown"; diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 5c354ac3d1..e3fce4a82a 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -10,7 +10,6 @@ using System.Threading; using Newtonsoft.Json; using osu.Framework.Logging; using osu.Framework.Platform; -using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO.Archives; @@ -21,8 +20,6 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens; -using osu.Game.Screens.Import; using Realms; namespace osu.Game.Scoring @@ -31,8 +28,6 @@ namespace osu.Game.Scoring { public override IEnumerable HandledExtensions => new[] { ".osr" }; - public IPerformFromScreenRunner? Performer { get; set; } - protected override string[] HashableFileTypes => new[] { ".osr" }; private readonly RulesetStore rulesets; @@ -69,9 +64,6 @@ namespace osu.Game.Scoring private void onMissingBeatmap(LegacyScoreDecoder.BeatmapNotFoundException e, ArchiveReader archive, string name) { - if (Performer == null) - return; - var stream = new MemoryStream(); // stream will close after exception throw, so fetch the stream again. @@ -87,7 +79,7 @@ namespace osu.Game.Scoring req.Success += res => { - Performer.PerformFromScreen(screen => screen.Push(new ReplayMissingBeatmapScreen(res, stream))); + PostNotification?.Invoke(new MissingBeatmapNotification(res, stream)); }; api.Queue(req); diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 9331168ab0..31b5bd8365 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -21,7 +21,6 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; using osu.Game.Online.API; using osu.Game.Scoring.Legacy; -using osu.Game.Screens; namespace osu.Game.Scoring { @@ -31,12 +30,6 @@ namespace osu.Game.Scoring private readonly ScoreImporter scoreImporter; private readonly LegacyScoreExporter scoreExporter; - [CanBeNull] - public IPerformFromScreenRunner Performer - { - set => scoreImporter.Performer = value; - } - public override bool PauseImports { get => base.PauseImports; diff --git a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs deleted file mode 100644 index 614d652f47..0000000000 --- a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs +++ /dev/null @@ -1,199 +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.Collections.Generic; -using System.IO; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Audio; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables.Cards; -using osu.Game.Configuration; -using osu.Game.Database; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Overlays; -using osu.Game.Overlays.Settings; -using osu.Game.Scoring; -using osu.Game.Screens.Ranking; -using osuTK; -using Realms; - -namespace osu.Game.Screens.Import -{ - [Cached(typeof(IPreviewTrackOwner))] - public partial class ReplayMissingBeatmapScreen : OsuScreen, IPreviewTrackOwner - { - [Resolved] - private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; - - [Resolved] - private ScoreManager scoreManager { get; set; } = null!; - - [Resolved] - private RealmAccess realm { get; set; } = null!; - - private IDisposable? realmSubscription; - - [Cached] - private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - - [Resolved] - private INotificationOverlay? notificationOverlay { get; set; } - - private Container beatmapPanelContainer = null!; - private ReplayDownloadButton replayDownloadButton = null!; - private SettingsCheckbox automaticDownload = null!; - - private readonly MemoryStream scoreStream; - - private readonly APIBeatmapSet beatmapSetInfo; - - public ReplayMissingBeatmapScreen(APIBeatmap beatmap, MemoryStream scoreStream) - { - beatmapSetInfo = beatmap.BeatmapSet!; - - this.scoreStream = scoreStream; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours, OsuConfigManager config) - { - InternalChildren = new Drawable[] - { - new Container - { - Masking = true, - CornerRadius = 20, - AutoSizeAxes = Axes.Both, - AutoSizeDuration = 500, - AutoSizeEasing = Easing.OutQuint, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Box - { - Colour = colours.Gray5, - RelativeSizeAxes = Axes.Both, - }, - new FillFlowContainer - { - Margin = new MarginPadding(20), - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spacing = new Vector2(15), - Children = new Drawable[] - { - new OsuSpriteText - { - Text = "Beatmap info", - Font = OsuFont.Default.With(size: 30), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spacing = new Vector2(15), - Children = new Drawable[] - { - beatmapPanelContainer = new Container - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - } - }, - automaticDownload = new SettingsCheckbox - { - LabelText = "Automatically download beatmaps", - Current = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - replayDownloadButton = new ReplayDownloadButton(new ScoreInfo()) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - } - } - } - }, - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - updateStatus(); - realmSubscription = realm.RegisterForNotifications( - realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); - } - - private void updateStatus() - { - beatmapPanelContainer.Clear(); - beatmapPanelContainer.Child = new BeatmapCardNormal(beatmapSetInfo, allowExpansion: false); - checkForAutomaticDownload(beatmapSetInfo); - } - - private void checkForAutomaticDownload(APIBeatmapSet beatmap) - { - if (!automaticDownload.Current.Value) - return; - - beatmapDownloader.Download(beatmap); - } - - private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) - { - if (changes?.InsertedIndices == null) return; - - if (!scoreStream.CanRead) return; - - if (sender.Any(b => b.OnlineID == beatmapSetInfo.OnlineID)) - { - var progressNotification = new ImportProgressNotification(); - var importTask = new ImportTask(scoreStream, "score.osr"); - scoreManager.Import(progressNotification, new[] { importTask }) - .ContinueWith(s => - { - s.GetResultSafely>>().FirstOrDefault()?.PerformRead(score => - { - Guid scoreid = score.ID; - Scheduler.Add(() => - { - replayDownloadButton.Score.Value = realm.Realm.Find(scoreid) ?? new ScoreInfo(); - }); - }); - }); - - notificationOverlay?.Post(progressNotification); - - realmSubscription?.Dispose(); - } - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - realmSubscription?.Dispose(); - } - } -} From 5abf271b56e24adeecffbcb60ca3edd8c62e667c Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 3 Sep 2023 21:49:29 -0700 Subject: [PATCH 1259/2100] Implement beatmap options popover --- .../SongSelect/TestSceneSongSelectFooterV2.cs | 7 +- .../Select/FooterV2/BeatmapOptionsPopover.cs | 148 ++++++++++++++++++ .../Select/FooterV2/FooterButtonOptionsV2.cs | 37 ++++- osu.Game/Screens/Select/FooterV2/FooterV2.cs | 10 +- osu.Game/Screens/Select/SongSelect.cs | 18 +-- 5 files changed, 205 insertions(+), 15 deletions(-) create mode 100644 osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs index 72adbfc104..ed2ae67ae5 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -37,10 +38,10 @@ namespace osu.Game.Tests.Visual.SongSelect Children = new Drawable[] { - footer = new FooterV2 + new PopoverContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre + RelativeSizeAxes = Axes.Both, + Child = footer = new FooterV2(), }, overlay = new DummyOverlay() }; diff --git a/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs b/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs new file mode 100644 index 0000000000..ec35c6ff38 --- /dev/null +++ b/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs @@ -0,0 +1,148 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Collections; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; +using osuTK.Input; + +namespace osu.Game.Screens.Select.FooterV2 +{ + public partial class BeatmapOptionsPopover : OsuPopover + { + private FillFlowContainer buttonFlow = null!; + private readonly FooterButtonOptionsV2 footerButton; + + public BeatmapOptionsPopover(FooterButtonOptionsV2 footerButton) + { + this.footerButton = footerButton; + } + + [BackgroundDependencyLoader] + private void load(ManageCollectionsDialog? manageCollectionsDialog, SongSelect? songSelect, OsuColour colours) + { + Content.Padding = new MarginPadding(5); + + Child = buttonFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(3), + }; + + addButton(@"Manage collections", FontAwesome.Solid.Book, () => manageCollectionsDialog?.Show()); + addButton(@"Delete all difficulties", FontAwesome.Solid.Trash, () => songSelect?.DeleteBeatmap(), colours.Red); + addButton(@"Remove from unplayed", FontAwesome.Regular.TimesCircle, null); + addButton(@"Clear local scores", FontAwesome.Solid.Eraser, () => songSelect?.ClearScores()); + + if (songSelect != null && songSelect.AllowEditing) + addButton(@"Edit beatmap", FontAwesome.Solid.PencilAlt, () => songSelect.Edit()); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(this)); + } + + private void addButton(LocalisableString text, IconUsage icon, Action? action, Color4? colour = null) + { + var button = new OptionButton + { + Text = text, + Icon = icon, + TextColour = colour, + Action = () => + { + Hide(); + action?.Invoke(); + }, + }; + + buttonFlow.Add(button); + } + + private partial class OptionButton : OsuButton + { + public IconUsage Icon { get; init; } + public Color4? TextColour { get; init; } + + public OptionButton() + { + Size = new Vector2(265, 50); + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + BackgroundColour = colourProvider.Background3; + + SpriteText.Colour = TextColour ?? Color4.White; + Content.CornerRadius = 10; + + Add(new SpriteIcon + { + Blending = BlendingParameters.Additive, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(17), + X = 15, + Icon = Icon, + Colour = TextColour ?? Color4.White, + }); + } + + protected override SpriteText CreateText() => new OsuSpriteText + { + Depth = -1, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + X = 40 + }; + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + // don't absorb control as ToolbarRulesetSelector uses control + number to navigate + if (e.ControlPressed) return false; + + if (!e.Repeat && e.Key >= Key.Number1 && e.Key <= Key.Number9) + { + int requested = e.Key - Key.Number1; + + OptionButton? found = buttonFlow.Children.ElementAtOrDefault(requested); + + if (found != null) + { + found.TriggerClick(); + return true; + } + } + + return base.OnKeyDown(e); + } + + protected override void UpdateState(ValueChangedEvent state) + { + base.UpdateState(state); + + if (state.NewValue == Visibility.Hidden) + footerButton.IsActive.Value = false; + } + } +} diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs index 87cca0042a..a1559d32dc 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs @@ -2,14 +2,21 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Input.Bindings; namespace osu.Game.Screens.Select.FooterV2 { - public partial class FooterButtonOptionsV2 : FooterButtonV2 + public partial class FooterButtonOptionsV2 : FooterButtonV2, IHasPopover { + public readonly BindableBool IsActive = new BindableBool(); + [BackgroundDependencyLoader] private void load(OsuColour colour) { @@ -17,6 +24,34 @@ namespace osu.Game.Screens.Select.FooterV2 Icon = FontAwesome.Solid.Cog; AccentColour = colour.Purple1; Hotkey = GlobalAction.ToggleBeatmapOptions; + + Action = () => IsActive.Toggle(); } + + protected override void LoadComplete() + { + base.LoadComplete(); + + IsActive.BindValueChanged(active => + { + OverlayState.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden; + }); + + OverlayState.BindValueChanged(state => + { + switch (state.NewValue) + { + case Visibility.Hidden: + this.HidePopover(); + break; + + case Visibility.Visible: + this.ShowPopover(); + break; + } + }); + } + + public Popover GetPopover() => new BeatmapOptionsPopover(this); } } diff --git a/osu.Game/Screens/Select/FooterV2/FooterV2.cs b/osu.Game/Screens/Select/FooterV2/FooterV2.cs index cd95f3eb6c..0529f0d082 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterV2.cs @@ -48,11 +48,17 @@ namespace osu.Game.Screens.Select.FooterV2 private FillFlowContainer buttons = null!; - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + public FooterV2() { RelativeSizeAxes = Axes.X; Height = height; + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { InternalChildren = new Drawable[] { new Box diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 58755878d0..4ce7a6167e 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -311,9 +311,9 @@ namespace osu.Game.Screens.Select Footer.AddButton(button, overlay); BeatmapOptions.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, () => manageCollectionsDialog?.Show()); - BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo)); + BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, DeleteBeatmap); BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null); - BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo)); + BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, ClearScores); } sampleChangeDifficulty = audio.Samples.Get(@"SongSelect/select-difficulty"); @@ -916,18 +916,18 @@ namespace osu.Game.Screens.Select return true; } - private void delete(BeatmapSetInfo? beatmap) + public void DeleteBeatmap() { - if (beatmap == null) return; + if (Beatmap.Value.BeatmapSetInfo == null) return; - dialogOverlay?.Push(new BeatmapDeleteDialog(beatmap)); + dialogOverlay?.Push(new BeatmapDeleteDialog(Beatmap.Value.BeatmapSetInfo)); } - private void clearScores(BeatmapInfo? beatmapInfo) + public void ClearScores() { - if (beatmapInfo == null) return; + if (Beatmap.Value.BeatmapInfo == null) return; - dialogOverlay?.Push(new BeatmapClearScoresDialog(beatmapInfo, () => + dialogOverlay?.Push(new BeatmapClearScoresDialog(Beatmap.Value.BeatmapInfo, () => // schedule done here rather than inside the dialog as the dialog may fade out and never callback. Schedule(() => BeatmapDetails.Refresh()))); } @@ -963,7 +963,7 @@ namespace osu.Game.Screens.Select if (e.ShiftPressed) { if (!Beatmap.IsDefault) - delete(Beatmap.Value.BeatmapSetInfo); + DeleteBeatmap(); return true; } From 6c0bd13308589a8efcc2ba83738bc6ed0c2c6ca9 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 4 Sep 2023 00:21:48 -0700 Subject: [PATCH 1260/2100] Add xmldoc to newly exposed methods --- osu.Game/Screens/Select/SongSelect.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 4ce7a6167e..f6fc55b2a5 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -916,6 +916,9 @@ namespace osu.Game.Screens.Select return true; } + /// + /// Request to delete the current beatmap. + /// public void DeleteBeatmap() { if (Beatmap.Value.BeatmapSetInfo == null) return; @@ -923,6 +926,9 @@ namespace osu.Game.Screens.Select dialogOverlay?.Push(new BeatmapDeleteDialog(Beatmap.Value.BeatmapSetInfo)); } + /// + /// Request to clear the scores of the current beatmap. + /// public void ClearScores() { if (Beatmap.Value.BeatmapInfo == null) return; From 3decadaf519c26a6c4b941d065d41ce441589821 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Mon, 4 Sep 2023 16:18:14 +0900 Subject: [PATCH 1261/2100] use realm query --- osu.Game/Beatmaps/BeatmapManager.cs | 3 ++- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index d71d7b7f67..1f551f1218 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -26,6 +26,7 @@ using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Skinning; using osu.Game.Utils; +using Realms; namespace osu.Game.Beatmaps { @@ -284,7 +285,7 @@ namespace osu.Game.Beatmaps /// /// The query. /// The first result for the provided query, or null if no results were found. - public BeatmapInfo? QueryBeatmap(Expression> query) => Realm.Run(r => r.All().FirstOrDefault(query)?.Detach()); + public BeatmapInfo? QueryBeatmap(Expression> query) => Realm.Run(r => r.All().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach()); /// /// A default representation of a WorkingBeatmap to use when no beatmap is available. diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index c06f4da4ae..78eed626f2 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -88,7 +88,7 @@ namespace osu.Game.Beatmaps public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) { - if (beatmapInfo?.BeatmapSet == null || beatmapInfo.BeatmapSet?.DeletePending == true) + if (beatmapInfo?.BeatmapSet == null) return DefaultBeatmap; lock (workingCache) From 164f61f59034f2d713cdb4676f7327a4f41b512f Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Mon, 4 Sep 2023 17:14:04 +0900 Subject: [PATCH 1262/2100] clean up --- .../Database/MissingBeatmapNotification.cs | 53 +++++++------------ 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index 2587160a57..7a39c6307b 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Configuration; @@ -30,21 +31,12 @@ namespace osu.Game.Database [Resolved] private ScoreManager scoreManager { get; set; } = null!; - [Cached] - private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - - [Resolved] - private BeatmapSetOverlay? beatmapSetOverlay { get; set; } - - private Container beatmapPanelContainer = null!; - private readonly MemoryStream scoreStream; private readonly APIBeatmapSet beatmapSetInfo; - private BeatmapDownloadTracker? downloadTracker; - private Bindable autodownloadConfig = null!; + private Bindable noVideoSetting = null!; public MissingBeatmapNotification(APIBeatmap beatmap, MemoryStream scoreStream) { @@ -54,35 +46,25 @@ namespace osu.Game.Database } [BackgroundDependencyLoader] - private void load(OsuColour colours, OsuConfigManager config) + private void load(OsuConfigManager config, BeatmapSetOverlay? beatmapSetOverlay) { + BeatmapDownloadTracker downloadTracker = new BeatmapDownloadTracker(beatmapSetInfo); + downloadTracker.State.BindValueChanged(downloadStatusChanged); + autodownloadConfig = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating); + noVideoSetting = config.GetBindable(OsuSetting.PreferNoVideo); Text = "You do not have the required beatmap for this replay"; - Content.Add(beatmapPanelContainer = new ClickableContainer + Content.Add(new ClickableContainer { RelativeSizeAxes = Axes.X, Height = 70, Anchor = Anchor.CentreLeft, Origin = Anchor.TopLeft, - Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapSetInfo.OnlineID) - }); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - downloadTracker = new BeatmapDownloadTracker(beatmapSetInfo); - downloadTracker.State.BindValueChanged(downloadStatusChanged, true); - - beatmapPanelContainer.Clear(); - beatmapPanelContainer.Child = new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, CornerRadius = 4, + Masking = true, + Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapSetInfo.OnlineID), Children = new Drawable[] { downloadTracker, @@ -125,7 +107,7 @@ namespace osu.Game.Database } } }, - new DownloadButton + new BeatmapDownloadButton(beatmapSetInfo) { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, @@ -134,15 +116,18 @@ namespace osu.Game.Database Margin = new MarginPadding { Bottom = 1f - }, - Action = () => beatmapDownloader.Download(beatmapSetInfo), - State = { BindTarget = downloadTracker.State } + } } } - }; + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); if (autodownloadConfig.Value) - beatmapDownloader.Download(beatmapSetInfo); + beatmapDownloader.Download(beatmapSetInfo, noVideoSetting.Value); } private void downloadStatusChanged(ValueChangedEvent status) From f68a12003a6df0dc32d9eefc391ec5228d1e9ddf Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Mon, 4 Sep 2023 17:37:31 +0900 Subject: [PATCH 1263/2100] check beatmap hash before try to import --- .../Database/MissingBeatmapNotification.cs | 33 ++++++++++++++----- osu.Game/Scoring/ScoreImporter.cs | 2 +- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index 7a39c6307b..92b33e20be 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -2,18 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using System.IO; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; @@ -31,31 +30,43 @@ namespace osu.Game.Database [Resolved] private ScoreManager scoreManager { get; set; } = null!; - private readonly MemoryStream scoreStream; + [Resolved] + private RealmAccess realm { get; set; } = null!; + private readonly MemoryStream scoreStream; private readonly APIBeatmapSet beatmapSetInfo; + private readonly string beatmapHash; private Bindable autodownloadConfig = null!; private Bindable noVideoSetting = null!; - public MissingBeatmapNotification(APIBeatmap beatmap, MemoryStream scoreStream) + public MissingBeatmapNotification(APIBeatmap beatmap, MemoryStream scoreStream, string beatmapHash) { beatmapSetInfo = beatmap.BeatmapSet!; + this.beatmapHash = beatmapHash; this.scoreStream = scoreStream; } [BackgroundDependencyLoader] private void load(OsuConfigManager config, BeatmapSetOverlay? beatmapSetOverlay) { + Text = "You do not have the required beatmap for this replay"; + + realm.Run(r => + { + if (r.All().Any(s => s.OnlineID == beatmapSetInfo.OnlineID)) + { + Text = "You have the corresponding beatmapset but no beatmap, you may need to update the beatmap."; + } + }); + BeatmapDownloadTracker downloadTracker = new BeatmapDownloadTracker(beatmapSetInfo); downloadTracker.State.BindValueChanged(downloadStatusChanged); autodownloadConfig = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating); noVideoSetting = config.GetBindable(OsuSetting.PreferNoVideo); - Text = "You do not have the required beatmap for this replay"; - Content.Add(new ClickableContainer { RelativeSizeAxes = Axes.X, @@ -135,8 +146,14 @@ namespace osu.Game.Database if (status.NewValue != DownloadState.LocallyAvailable) return; - var importTask = new ImportTask(scoreStream, "score.osr"); - scoreManager.Import(this, new[] { importTask }); + realm.Run(r => + { + if (r.All().Any(s => s.MD5Hash == beatmapHash)) + { + var importTask = new ImportTask(scoreStream, "score.osr"); + scoreManager.Import(this, new[] { importTask }); + } + }); } } } diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index e3fce4a82a..650e25a512 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -79,7 +79,7 @@ namespace osu.Game.Scoring req.Success += res => { - PostNotification?.Invoke(new MissingBeatmapNotification(res, stream)); + PostNotification?.Invoke(new MissingBeatmapNotification(res, stream, e.Hash)); }; api.Queue(req); From 87aa191c121214c18e0a4270e7f20f856da40ab2 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Mon, 4 Sep 2023 17:53:12 +0900 Subject: [PATCH 1264/2100] use realm Subscription instead of Beatmap Download Tracker --- .../Database/MissingBeatmapNotification.cs | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index 92b33e20be..86522d0864 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.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.IO; using System.Linq; using osu.Framework.Allocation; @@ -13,12 +14,12 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Scoring; using osuTK.Graphics; +using Realms; namespace osu.Game.Database { @@ -40,6 +41,8 @@ namespace osu.Game.Database private Bindable autodownloadConfig = null!; private Bindable noVideoSetting = null!; + private IDisposable? realmSubscription; + public MissingBeatmapNotification(APIBeatmap beatmap, MemoryStream scoreStream, string beatmapHash) { beatmapSetInfo = beatmap.BeatmapSet!; @@ -53,17 +56,17 @@ namespace osu.Game.Database { Text = "You do not have the required beatmap for this replay"; + realmSubscription = realm.RegisterForNotifications( + realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); + realm.Run(r => { - if (r.All().Any(s => s.OnlineID == beatmapSetInfo.OnlineID)) + if (r.All().Any(s => !s.DeletePending && s.OnlineID == beatmapSetInfo.OnlineID)) { - Text = "You have the corresponding beatmapset but no beatmap, you may need to update the beatmap."; + Text = "You have the corresponding beatmapset but no beatmap, you may need to update the beatmapset."; } }); - BeatmapDownloadTracker downloadTracker = new BeatmapDownloadTracker(beatmapSetInfo); - downloadTracker.State.BindValueChanged(downloadStatusChanged); - autodownloadConfig = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating); noVideoSetting = config.GetBindable(OsuSetting.PreferNoVideo); @@ -78,7 +81,6 @@ namespace osu.Game.Database Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapSetInfo.OnlineID), Children = new Drawable[] { - downloadTracker, new DelayedLoadWrapper(() => new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.Card) { OnlineInfo = beatmapSetInfo, @@ -141,19 +143,16 @@ namespace osu.Game.Database beatmapDownloader.Download(beatmapSetInfo, noVideoSetting.Value); } - private void downloadStatusChanged(ValueChangedEvent status) + private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) { - if (status.NewValue != DownloadState.LocallyAvailable) - return; + if (changes?.InsertedIndices == null) return; - realm.Run(r => + if (sender.Any(s => s.Beatmaps.Any(b => b.MD5Hash == beatmapHash))) { - if (r.All().Any(s => s.MD5Hash == beatmapHash)) - { - var importTask = new ImportTask(scoreStream, "score.osr"); - scoreManager.Import(this, new[] { importTask }); - } - }); + var importTask = new ImportTask(scoreStream, "score.osr"); + scoreManager.Import(this, new[] { importTask }); + realmSubscription?.Dispose(); + } } } } From fd1fce486a18c6b12859b3fa197c707bac583751 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Tue, 5 Sep 2023 00:21:08 +0900 Subject: [PATCH 1265/2100] ensure dispose realmSubscription --- osu.Game/Database/MissingBeatmapNotification.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index 86522d0864..d6674b9434 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -154,5 +154,11 @@ namespace osu.Game.Database realmSubscription?.Dispose(); } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + realmSubscription?.Dispose(); + } } } From f616648730ac9fd438cc2d15aeb7eb065f875cae Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 4 Sep 2023 09:40:35 -0700 Subject: [PATCH 1266/2100] Remove icon blending --- osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs b/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs index ec35c6ff38..4e1334fd11 100644 --- a/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs +++ b/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs @@ -97,7 +97,6 @@ namespace osu.Game.Screens.Select.FooterV2 Add(new SpriteIcon { - Blending = BlendingParameters.Additive, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Size = new Vector2(17), From bf71099e5743efb966482d5b145a551b0208729f Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 4 Sep 2023 11:34:21 -0700 Subject: [PATCH 1267/2100] Fix truncating sprite text usage --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index b7b60cffab..7821aa5be0 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -280,15 +280,14 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.X, Children = new Drawable[] { - TitleLabel = new OsuSpriteText + TitleLabel = new TruncatingSpriteText { Shadow = true, Current = { BindTarget = titleBinding }, Font = OsuFont.TorusAlternate.With(size: 40, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Truncate = true }, - ArtistLabel = new OsuSpriteText + ArtistLabel = new TruncatingSpriteText { // TODO : figma design has a diffused shadow, instead of the solid one present here, not possible currently as far as i'm aware. Shadow = true, @@ -296,7 +295,6 @@ namespace osu.Game.Screens.Select // Not sure if this should be semi bold or medium Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Truncate = true } } } From e8a793425bf28e19ba3667c8c712c4db96fc09fa Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 4 Sep 2023 12:38:40 -0700 Subject: [PATCH 1268/2100] Use right padding instead of negative x offset --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index 7821aa5be0..284f14cd9e 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -155,18 +155,21 @@ namespace osu.Game.Screens.Select LoadComponentAsync(loadingInfo = new Container { - Masking = true, - // We offset this by the portion of the colour bar underneath we wish to show - X = -colour_bar_width, - CornerRadius = corner_radius, + Padding = new MarginPadding { Right = colour_bar_width }, RelativeSizeAxes = Axes.Both, Depth = DisplayedContent?.Depth + 1 ?? 0, - Children = new Drawable[] + Child = new Container { - // TODO: New wedge design uses a coloured horizontal gradient for its background, however this lacks implementation information in the figma draft. - // pending https://www.figma.com/file/DXKwqZhD5yyb1igc3mKo1P?node-id=2980:3361#340801912 being answered. - new BeatmapInfoWedgeBackground(beatmap) { Shear = -Shear }, - Info = new WedgeInfoText(beatmap) { Shear = -Shear } + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + // TODO: New wedge design uses a coloured horizontal gradient for its background, however this lacks implementation information in the figma draft. + // pending https://www.figma.com/file/DXKwqZhD5yyb1igc3mKo1P?node-id=2980:3361#340801912 being answered. + new BeatmapInfoWedgeBackground(beatmap) { Shear = -Shear }, + Info = new WedgeInfoText(beatmap) { Shear = -Shear } + } } }, loaded => { From 2df20027355ddbf24bd43401ef5feabf5a7e1823 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 4 Sep 2023 12:42:01 -0700 Subject: [PATCH 1269/2100] Move negative corner radius margin to constructor --- .../SongSelect/TestSceneBeatmapInfoWedgeV2.cs | 16 ++++++++++------ osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 1 + 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs index 09b93119cc..ae5b739c4d 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs @@ -50,13 +50,17 @@ namespace osu.Game.Tests.Visual.SongSelect RelativeSizeAxes = Axes.X, Margin = new MarginPadding { Top = 20, Left = -10 } }, - infoWedge = new TestBeatmapInfoWedgeV2 + new Container { - State = { Value = Visibility.Visible }, - Width = 0.6f, - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = 20, Left = -10 } - }, + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 20 }, + Child = infoWedge = new TestBeatmapInfoWedgeV2 + { + State = { Value = Visibility.Visible }, + Width = 0.6f, + RelativeSizeAxes = Axes.X, + }, + } }); AddSliderStep("change star difficulty", 0, 11.9, 5.55, v => diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index 284f14cd9e..742d9011b9 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -55,6 +55,7 @@ namespace osu.Game.Screens.Select Height = WEDGE_HEIGHT; Shear = wedged_container_shear; Masking = true; + Margin = new MarginPadding { Left = -corner_radius }; EdgeEffect = new EdgeEffectParameters { Colour = Colour4.Black.Opacity(0.2f), From e70510ef191ef345eee35b490a12118fbb29a755 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 4 Sep 2023 12:56:20 -0700 Subject: [PATCH 1270/2100] Move drawable and binding logic to standard places --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index 742d9011b9..de3e634819 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -46,9 +46,9 @@ namespace osu.Game.Screens.Select protected WedgeInfoText? Info { get; private set; } - private readonly Container difficultyColourBar; - private readonly StarCounter starCounter; - private readonly BufferedContainer bufferedContent; + private Container difficultyColourBar = null!; + private StarCounter starCounter = null!; + private BufferedContainer bufferedContent = null!; public BeatmapInfoWedgeV2() { @@ -63,7 +63,11 @@ namespace osu.Game.Screens.Select Radius = 3, }; CornerRadius = corner_radius; + } + [BackgroundDependencyLoader] + private void load() + { // We want to buffer the wedge to avoid weird transparency overlaps between the colour bar and the background. Child = bufferedContent = new BufferedContainer(pixelSnapping: true) { @@ -107,9 +111,10 @@ namespace osu.Game.Screens.Select }; } - [BackgroundDependencyLoader] - private void load() + protected override void LoadComplete() { + base.LoadComplete(); + ruleset.BindValueChanged(_ => updateDisplay()); } @@ -228,6 +233,8 @@ namespace osu.Game.Screens.Select public WedgeInfoText(WorkingBeatmap working) { this.working = working; + + RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] @@ -236,8 +243,6 @@ namespace osu.Game.Screens.Select var beatmapInfo = working.BeatmapInfo; var metadata = working.Metadata; - RelativeSizeAxes = Axes.Both; - titleBinding = localisation.GetLocalisedBindableString(new RomanisableString(metadata.TitleUnicode, metadata.Title)); artistBinding = localisation.GetLocalisedBindableString(new RomanisableString(metadata.ArtistUnicode, metadata.Artist)); From 82fb9dc2ef5eb03f2db25426f11a19f836e62f68 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 4 Sep 2023 12:59:21 -0700 Subject: [PATCH 1271/2100] Simplify text initialization --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index de3e634819..fd655c50ca 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -212,9 +212,6 @@ namespace osu.Game.Screens.Select private StarRatingDisplay starRatingDisplay = null!; - private ILocalisedBindableString titleBinding = null!; - private ILocalisedBindableString artistBinding = null!; - private readonly WorkingBeatmap working; public IBindable DisplayedStars => starRatingDisplay.DisplayedStars; @@ -238,14 +235,11 @@ namespace osu.Game.Screens.Select } [BackgroundDependencyLoader] - private void load(LocalisationManager localisation) + private void load() { var beatmapInfo = working.BeatmapInfo; var metadata = working.Metadata; - titleBinding = localisation.GetLocalisedBindableString(new RomanisableString(metadata.TitleUnicode, metadata.Title)); - artistBinding = localisation.GetLocalisedBindableString(new RomanisableString(metadata.ArtistUnicode, metadata.Artist)); - Children = new Drawable[] { new FillFlowContainer @@ -292,7 +286,7 @@ namespace osu.Game.Screens.Select TitleLabel = new TruncatingSpriteText { Shadow = true, - Current = { BindTarget = titleBinding }, + Text = new RomanisableString(metadata.TitleUnicode, metadata.Title), Font = OsuFont.TorusAlternate.With(size: 40, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, }, @@ -300,7 +294,7 @@ namespace osu.Game.Screens.Select { // TODO : figma design has a diffused shadow, instead of the solid one present here, not possible currently as far as i'm aware. Shadow = true, - Current = { BindTarget = artistBinding }, + Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist), // Not sure if this should be semi bold or medium Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, From e0a9c7e9a9680004b98a8086354fb758b65daa51 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 4 Sep 2023 14:54:58 -0700 Subject: [PATCH 1272/2100] Fix wedge showing abruptly in test --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs index ae5b739c4d..3236841dc3 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs @@ -56,7 +56,6 @@ namespace osu.Game.Tests.Visual.SongSelect Padding = new MarginPadding { Top = 20 }, Child = infoWedge = new TestBeatmapInfoWedgeV2 { - State = { Value = Visibility.Visible }, Width = 0.6f, RelativeSizeAxes = Axes.X, }, @@ -165,6 +164,7 @@ namespace osu.Game.Tests.Visual.SongSelect { containerBefore = infoWedge.DisplayedContent; infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b); + infoWedge.Show(); }); AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); From 854bb323cc09e0a296ebd2ca948e4a6074950044 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 4 Sep 2023 15:02:00 -0700 Subject: [PATCH 1273/2100] Remove weird red edge effect visibility --- .../Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs index 3236841dc3..827d23c0fc 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs @@ -7,7 +7,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; @@ -94,17 +93,6 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestWedgeVisibility() { - // Mostly just in case someone runs this test before others, - // leading to the shadow being very hard to see if it is black - AddStep("make shadow red for test visibility", () => - { - infoWedge.EdgeEffect = new EdgeEffectParameters - { - Colour = Colour4.Red, - Type = EdgeEffectType.Shadow, - Radius = 5, - }; - }); AddStep("hide", () => { infoWedge.Hide(); }); AddWaitStep("wait for hide", 3); AddAssert("check visibility", () => infoWedge.Alpha == 0); From 9accd0ded262e451792a10013762e442b5523441 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 4 Sep 2023 15:02:38 -0700 Subject: [PATCH 1274/2100] Fix star rating rolling counter regression --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index fd655c50ca..5316b4620b 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -314,7 +314,7 @@ namespace osu.Game.Screens.Select starRatingDisplay.Current.Value = s.NewValue ?? default; // Don't roll the counter on initial display (but still allow it to roll on applying mods etc.) - if (starRatingDisplay.Alpha > 0) + if (!starRatingDisplay.IsPresent) starRatingDisplay.FinishTransforms(true); starRatingDisplay.FadeIn(transition_duration); From 98d027d207143532178e4588c279368abb391d8b Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 4 Sep 2023 21:34:53 -0700 Subject: [PATCH 1275/2100] Fix star counter animating weird and delayed --- osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs index 5316b4620b..de930ff837 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -187,11 +187,16 @@ namespace osu.Game.Screens.Select Info.DisplayedStars.BindValueChanged(s => { - starCounter.Current = (float)s.NewValue; starCounter.Colour = s.NewValue >= 6.5 ? colours.Orange1 : Colour4.Black.Opacity(0.75f); difficultyColourBar.FadeColour(colours.ForStarDifficulty(s.NewValue)); }, true); + + Info.ActualStars.BindValueChanged(s => + { + // use actual stars as star counter has its own animation + starCounter.Current = (float)s.NewValue; + }); }); }); @@ -215,6 +220,7 @@ namespace osu.Game.Screens.Select private readonly WorkingBeatmap working; public IBindable DisplayedStars => starRatingDisplay.DisplayedStars; + public Bindable ActualStars = new Bindable(); [Resolved] private IBindable> mods { get; set; } = null!; @@ -312,6 +318,7 @@ namespace osu.Game.Screens.Select starDifficulty.BindValueChanged(s => { starRatingDisplay.Current.Value = s.NewValue ?? default; + ActualStars.Value = s.NewValue?.Stars ?? 0; // Don't roll the counter on initial display (but still allow it to roll on applying mods etc.) if (!starRatingDisplay.IsPresent) From 94516133912a4c32048dddc52c53fb9696bb0477 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 4 Sep 2023 21:39:26 -0700 Subject: [PATCH 1276/2100] Rename null beatmap test to indicate there's a background --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs index 827d23c0fc..a8484caf1c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestNullBeatmap() + public void TestNullBeatmapWithBackground() { selectBeatmap(null); AddAssert("check default title", () => infoWedge.Info!.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title); From 1215ad7ace400cb404121d9a8ead9ac81f2ad2a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Sep 2023 15:40:00 +0900 Subject: [PATCH 1277/2100] Update framework --- 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 2d15bce85a..2bfdce5ab8 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + 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 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-apple.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-apple.png index 6a862572faca191c265bd1bdd76283c08fdab790..17f3be9c262953cac52da794d601bb853d8dc276 100755 GIT binary patch literal 4677 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 Hu0mjf9r&r4 literal 26685 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 B13Lf! diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-bananas-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-bananas-overlay.png index 9d1d6d1177b9e36bcef557251ba3914fa5acbae2..2c94ea78bfdba901614472a3215401d7b0a2dd71 100755 GIT binary patch literal 4949 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 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-bananas.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-bananas.png index 88bc33b592a04625c97fafdea8040090e64d5cfc..2c94ea78bfdba901614472a3215401d7b0a2dd71 100755 GIT binary patch literal 4949 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 Hu0mjfi%6-N literal 26684 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 Hu0mjfQGBVH literal 26684 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 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-grapes-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-grapes-overlay.png index 2529b5cd1928637c69a512b292d5533fee42a48c..17f3be9c262953cac52da794d601bb853d8dc276 100755 GIT binary patch literal 4677 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 Hu0mjf9r&r4 literal 26684 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 P00000NkvXXu0mjfY&D(f literal 26684 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 P00000NkvXXu0mjfY&D(f literal 26685 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 B;9~#) diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-orange.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-orange.png index 998f5fb3cb6f04dd8541efdc151bde4014aa69db..3dc60464cf31e3ba07d5585542d75d616f63da03 100755 GIT binary patch literal 4633 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 P00000NkvXXu0mjfY&D(f literal 26685 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 P00000NkvXXu0mjfY&D(f literal 26685 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 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-pear.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-pear.png index c8acaaa58349b47893c0c3ab3bbd4a2d472c8721..3dc60464cf31e3ba07d5585542d75d616f63da03 100755 GIT binary patch literal 4633 zcmV+!66WoRP)KLZ*U+5Lu!Sk^o_Z5E4Meg@_7P6crJiNL9pw)e1;Xm069{HJUZAPk55R%$-RIA z6-eL&AQ0xu!e<4=008gy@A0LT~suv4>S3ILP<0Bm`DLLvaF4FK%)Nj?Pt*r}7;7Xa9z9H|HZjR63e zC`Tj$K)V27Re@400>HumpsYY5E(E}?0f1SyGDiY{y#)Yvj#!WnKwtoXnL;eg03bL5 z07D)V%>y7z1E4U{zu>7~aD})?0RX_umCct+(lZpemCzb@^6=o|A>zVpu|i=NDG+7} zl4`aK{0#b-!z=TL9Wt0BGO&T{GJWpjryhdijfaIQ&2!o}p04JRKYg3k&Tf zVxhe-O!X z{f;To;xw^bEES6JSc$k$B2CA6xl)ltA<32E66t?3@gJ7`36pmX0IY^jz)rRYwaaY4 ze(nJRiw;=Qb^t(r^DT@T3y}a2XEZW-_W%Hszxj_qD**t_m!#tW0KDiJT&R>6OvVTR z07RgHDzHHZ48atvzz&?j9lXF70$~P3Knx_nJP<+#`N z#-MZ2bTkiLfR>_b(HgWKJ%F~Nr_oF3b#wrIijHG|(J>BYjM-sajE6;FiC7vY#};Gd zST$CUHDeuEH+B^pz@B062qXfFfD`NpUW5?BY=V%GM_5c)L#QR}BeW8_2v-S%gfYS= zB9o|3v?Y2H`NVi)In3rTB8+ej^> zQ=~r95NVuDChL%G$=>7$vVg20myx%S50Foi`^m%Pw-h?Xh~i8Mq9jtJloCocWk2Nv zrJpiFnV_ms&8eQ$2&#xWpIS+6pmtC%Q-`S&GF4Q#^mhymh7E(qNMa}%YZ-ePrx>>xFPTiH1=E+A$W$=bG8>s^ zm=Bn5Rah$aDtr}@$`X}2l~$F0mFKEdRdZE8)p@E5RI61Ft6o-prbbn>P~)iy)E2AN zsU20jsWz_8Qg>31P|s0cqrPALg8E|(vWA65poU1JRAaZs8I2(p#xiB`SVGovRs-uS zYnV-9TeA7=Om+qP8+I>yOjAR1s%ETak!GFdam@h^# z)@rS0t$wXH+Irf)+G6c;?H29p+V6F6oj{!|o%K3xI`?%6x;DB|x`n#ibhIR?(H}Q3Gzd138Ei2)WAMz7W9Vy`X}HnwgyEn!VS)>mv$8&{hQn>w4zwy3R}t;BYlZQm5)6pty=DfLrs+A-|>>;~;Q z_F?uV_HFjh9n2gO9o9Q^JA86v({H5aB!kjoO6 zc9$1ZZKsN-Zl8L~mE{`ly3)1N^`o1+o7}D0ZPeY&J;i;i`%NyJ8_8Y6J?}yE@b_5a zam?eLr<8@mESk|3$_SkmS{wQ>%qC18))9_|&j{ZT zes8AvOzF(F2#DZEY>2oYX&IRp`F#{ADl)1r>QS^)ba8a|EY_^#S^HO&t^Rgqwv=MZThqqEWH8 zxJo>d=ABlR_Bh=;eM9Tw|Ih34~oTE|= zX_mAr*D$vzw@+p(E0Yc6dFE}(8oqt`+R{gE3x4zjX+Sb3_cYE^= zgB=w+-tUy`ytONMS8KgRef4hA?t0j zufM;t32jm~jUGrkaOInTZ`zyfns>EuS}G30LFK_G-==(f<51|K&cocp&EJ`SxAh3? zNO>#LI=^+SEu(FqJ)ynt=!~PC9bO$rzPJB=?=j6w@a-(u02P7 zaQ)#(uUl{HW%tYNS3ItC^iAtK(eKlL`f9+{bJzISE?u8_z3;~C8@FyI-5j_jy7l;W z_U#vU3hqqYU3!mrul&B+{ptt$59)uk{;_4iZQ%G|z+lhASr6|H35TBkl>gI*;nGLU zN7W-nBaM%pA0HbH8olyl&XeJ%vZoWz%6?Y=dFykl=imL}`%BMQ{Mhgd`HRoLu6e2R za__6DuR6yg#~-}Tc|Gx_{H@O0eebyMy5GmWADJlpK>kqk(fVV@r_fLLKIeS?{4e)} z^ZO;zpECde2XaY7K~#7F<-uD_ly@G-;qRahf|3Z-ZzJoZ%v^JRr=^4EUKa4`}5gr`gXYUS*!VgGIc>VH$}r79D)U zVcueqk`=Qk;UKL%uT~CH!Ym~ZW>Cr}gqcWTKB1HuN_6B9qK(PYMu;3GDvCKvKO)%*DwH6opo>_IWNKpZBG-x4 zLcS6R`Lq!0IxlMCu$Vikf-GF<8JIVuc7O0TCcXtUD~w^J5-2aQdiH z0;7sPoEv27`4HsaIK8~11jjq{;xrP}_z|EEr%76Je>wh9K8)P7zR&A;2k|YXmhGe2CLRx{?s- zwBQ`n=uu1`CQQDP6#0ZPeH3eSNaYqzg_0PRIJZdEXs{dSgpwTp#M!N9Baf$;`(!A| zk->e;Q}XmIoW&`V2T+D{RwH8-&gb$7PT{Q5NNB=zvsfNMHcv238XjwLcF9B7jZ>=O za2eCV9C-+H=)hdo(D*&hHhBzNaaL$3oX512CXXSFcFcMG7CH1|s^vjcWBSR}V1zK; z%$Em|MK>m-c+BJn%u#t1M=)*7R4iV{DUwG~gj1?m9K*E8!)U>LDu-EgVs^;G*pBHU zC_@P*N{&2?Y@(PoGSp)JBafpQQ!B$wOpQE_8qD|l1q*SC7M#B`D(k0XUn z%-b5kN0@r~0QH!UHGoFUPWb?T!8B?B5lo4EfD%lEfXKy}FCSn&PM%nYX_HUThA9(U zFb(nv8ZcYLA@-L(B^K1S>G@ zB7#{apI{j#BDygz%O`jR(=D8A`2^YgN;ui_39|W>=*GM(pWqctw}@bt$tPHbiHL`o zV)+CsFzwZfQ2}P@;Hic7U>to%HbHMMIOd?m`@dp*KvyEQ54~nDi$+o!yJ)E@efQJGZl{zrkgBz5Sesi z_GvJ3>Bm&dgQ&*zldIq2Jf@vAc?@Z^W6o+obUe;Q}Xm|?7=y%B*zJykVb=4ZsAlYiBW-bn^cVs#q?o1$ybshpD?D66&gJb;Ixpg zBt$wbI0rNq1o;-{BmpHE0-VJ8mY~Llr9^P{E6MOzoCpOPJIaY-qP(jl!X~1aDCHU- zLO6X?DoIdDA5KW)MSwb-UdojOD5n>vj)2CGAeV4@sZ@fal3tuE1oeE#q!p)+DkU&B z(}&Yark)=QxQi2IF99VW0_-JughEXM3OPbIgY~k9 zpr#8;Imb}GV>?SUL0H0et}&EzEY);lHBAg9$`xu@PKw5f6qZxN6`~BKiPcIJl+w&_ zI;m$TCCt|-F`p84QcowtX{J<(hS&HP{fwlI298rr6&qMdAqC`tKGsO1b7xk)==o-p7EVcNOLMNYGyO}xrHO_~1#Zj~Jav*Fl4 P00000NkvXXu0mjfY&D(f literal 26686 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 B?Uw)m diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircle@2x.png index 207ce15dfabab74df8b5c13932506d94ef19dcfe..919d8f405ca7f40c921e71b91e7a81655db1f94b 100755 GIT binary patch delta 42344 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 delta 95844 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�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 K?H<|qr~Dr=#f2~c diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay-0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay-0@2x.png index ccd1494cdf1a9d19a9c860b88d240392ef1919ea..8e50cd033596fb3daacd173247d6779168cc8f99 100755 GIT binary patch literal 26595 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

KVsm2B>cb8^}lrej~MtL3IA_&{r^T6&VRN% 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

wSIutbvWi6D`m2u|!^@bLCI5us_1F3$vl-qM^r3 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≪}_ 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 E0Oy=DQvd(} diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-roll-end.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-roll-end.png deleted file mode 100644 index 60e2d3328f89df794d7b894fd2f6e2dfbb44ba82..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2095 zcmV+~2+;S5P)RCwC#opET>M-<0j)>w_GlupaE zQdA~lAy}x?Kb);qief>Tio%>y85B1MMFn+;e-wn4iCWkYhE~K;Tv@diJE>EKHPvca zHDYSh5db zjM@LdW5GVBo-aEjj#cOe{8^>_9`zyW-9figw}yN!BrqBam}foI8>u%0|NK)VUQ17MwEp_u)0SWA_zYSk0`vKNWN>hh+`oUH)YsRO?c29Ulj?)i_fk)eOCKgtn&0myLqkIW z_}bc9vSrJbXh!!!VSvLU|LoZ_a_7z+Qc+PsX3m@$3HVRcrwIkD813_csCRU9kmJXX z6SLW@7y57N1;PP`8XfgXXJ;olapDA-KYzYn@G|O2LINv>5pQpACr6GPAx5K75444P zy0E}XA9uN2q^YTi6c-okf$pcCCoHgHUayxp91e2w=(y;pc(#hyQZ9_ZIAR;C&Y<=sIbDl{);&l`C3{ zkDby0E2H=M^XIin&>U%i(fa!O$gyL`w02LDr2)qJ8y6kBckkvw?~?`?ZDeGGSglsF zeED)-YJV&Zuu_;{3Cf9by;Q(Te?D~R5D)rOsep0!1ecZh`T4v7EQtjy+zEiBq$J45 z$bgcP64qC+A8(ZE$iu{m6Co!jhX=h+x-%YAr%r{98<~O3Wc6iXfya#-2ejmb($dn1 zqAc_&p2?FZLt!EF@f0o}mq`VT1K6B7b724e{j9HSmI@ec%9JU*DC54iNh)BB^4z&| zp}4qMt$9-T;3dULJdE~SF&`#6bcqDT&UKB`_>;zdC7y* zkL3i$s*7P6QUaSyCT49SB`_`~ScAB<7q!f0^VkArEEVMio<4oL>eM7HFz$k=jc3vV zvj#3{f$?h6m;!dY-D)2XOAFlF+pG5Rh_t}n-Q8**JGnR0L|-+jSf!K^W(`;vVJU$< z9uKRjM@nF))5&VGO9_lOVaH5_aSX=0&uVip^?OFyfW2NXoI7_;t*ISPF-jLd@8I^`A_qm>)_PVY}U~28-4p z-SQCCC>k3ZSe(1O}uxL-S@US3|hq84=j z@BTwh=%PZ%D*)krm$6Vl`;B^w+Hy)rL&XTZm&l3kvhZc2&=G$6^eLP=b&A#85{xiN z5n+_LI(_;yFS>|!> zv6w*7k%I>h>PcaodPYsI^}Pb4a1iQO*45SV#DhW|pK6cln*`nqup-Nf>)2hpcIlN| z{u$oEn*-h{umXy5Q03+2#9%P!0rkW(?ZgFGkxs|ax2&v;m`o-9RCLv5(>+ z-D0tjJ$v>5_HeJ99AZ~o4Byv$vQ7rFn!Knt-MUo*7rT`o9)C-<(qUi|^hXXo0 zJAneiwQJW#<2@~C_)@BJ>OTh0Tg2Vo#hZAr{;iKk>T0I+D1+j5Dax2lg9Nj;DIMeQ z!6UhNhOsetc;sH9Z|X-1jIR)C4S{S-lztfvF#f|spJV(5Z{CR44sVX$;?GXe55fE+ ZzyR4Xd;=*Q?iv69002ovPDHLkV1gL3>M{TT diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-roll-middle.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-roll-middle.png deleted file mode 100644 index 03ebd1904875ab54fa71cbe948304fbb6266d69a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 140 zcmeAS@N?(olHy`uVBq!ia0vp^j6ht&!3HE(th_Z1NJ*BsMwA5Sr)zI%U~#c*aSiV>bgcqrenRo!(7sQ*l5b=5lS zHz>XqA5MduI3AJ3)o!d^br=$OWl&|sX@~VZ$$kjMf^*+5^|JIp^;7XGD@8l4m+Fq5 zbR@7C?o*Zte`1aIhsJvoa%oiq#8D}Ki%3xN-Ksnged2uiUDl?~c^n4)<4I1OL@J{s zVbM5~>zhOgK)+qf;w)+W21PgsudT$_W{dvh%J^~W25rSR8(jc< z_fL>Vf#GBu5+M8?a)ADqw~O-tr|7*i>Is87{CTFawH0p@a7Nez!b2*^x0zygxz-zJ zkY>w@5g3#`3)hxc7oS!>lk?q4A2}BKF4OE+l=$q;=o6RE{<#@Y^c6ReN2_0|6weK| z2uIAnbU>U^b5ZVub3{6?%JM{p_E&w1R}>YSKGQiTyB-Xk=tfFQe;f9 zT7frr1ow~=e}5}V$xMU*Y4I@*Evnes())7`go78n7;RKpIx`kD`E%XMaa-?OQZ-28 zi5A$#5ZvHTSuXzTXl?s@$h+&#_xArktA)i(04d2$ziJv9de^lR6uwRAw+mf3DxL5i zZ%jZ&vDSxmHFDjcMMePT4CJ&RQMrwF1c;wW1GqPo!cSZ^A(b-S3e=k6n)X>l7c;buJdiW4TJFtS@WP;S0XgZL zRZr~^B;3JzAm@^5`N8^|J9S$Eo}Uy8Bag4E`!ifO*#yn~@NB|Sx?P45|HI~V?QC?g z4jFo_Dw>(fI(BuPb+*GvzfZv!*mM-wnSs#~9FzwephKn{u!ofyos z#4hRx06epEi_y1?Qq$3HdB0QwjO&@~ShGCE8=pRb&BawoU$ZzWCoUoOIx7L1t28)d z`yS1UG#((0hLy3j40^L2$5CnL#>$kX73~K-M7th}=wN8AyWKeS(#q%ETnn=#%<90t ze7vB1Kb>x9o>VALdA9*Ak}r2xz8Rw57nC4M3H<_h<7Ki1Fz`+PZbuO%l5Br0a(oSA zg;^8KgM`6aeM%%kx-bcr>ZGgP7Qr&44d-K|2JBpyh%9M!COO<|4q{>(ULo9=0@1|` z{mdIm##t*?42H{S5)ek>iXW($sYURj-j(3}Ao(XED3+A5XEmxS;3@V8-Lioga8`Zm zKGNssuY$f>>S4+Fubmb-0Ub}(`9&iCk5&1fTkn{v_16n9O~0IyKJrn1yh6BoSU@*>AiDNP`V(J;9{Ld+x^0JNiTBTS>lAaos86NG z4tj6a1y0egJwb*di9UsPQkh zVUV*XtU<4yq=oM3s=AeRG6WkrM2iyo(WeWwm?<>=Qy7 zPz!0(bK?q3h@tu4v->WnEttwXV40o7A14u+VB^TJY0coQo|V0F9X(&cElEqk8XNmG iXXL~5fUGV&%!C*_ol6Q};|lnZLeiKIqOXgS)&B#53)X=E diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-drum-outer.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-drum-outer.png deleted file mode 100644 index 87ead80a08146adfa553d242f315f764a467506e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3749 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 DZJ4!X diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-roll-end.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-roll-end.png deleted file mode 100644 index 778c231f20ca9253e64ccffb64a0aa39a3b97a76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14570 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 PK2D*pzfZY$bjE)HM$sIi diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikobigcircle.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikobigcircle@2x.png similarity index 100% rename from osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikobigcircle.png rename to osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikobigcircle@2x.png diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikobigcircleoverlay.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikobigcircleoverlay.png deleted file mode 100644 index 6dca33317131cd69ef449c0a6c0122f0f39cd84c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66280 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 e__0p9W$exLq4HUNEC{AIAxiS~PfR=%CLeS{yi$ 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 Qd;A||`ImC#G8P~IA7%MrKL7v# diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikohitcircleoverlay.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikohitcircleoverlay.png deleted file mode 100644 index 2626c394dcdccdb7d74da5460ec3f6d6570ad9a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66281 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 z 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�kcUrvy?Ky7PM-eKS~7g% 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 From a9b45c6fdcba03437cca43f9613f85c60f98c0c2 Mon Sep 17 00:00:00 2001 From: Magnus-Cosmos Date: Tue, 19 Sep 2023 01:31:26 -0400 Subject: [PATCH 1465/2100] Fix slider path calculations for edge cases --- osu.Game/Rulesets/Objects/SliderPath.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 05960ec416..cf6d0d212b 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -261,10 +261,14 @@ namespace osu.Game.Rulesets.Objects // The current vertex ends the segment var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); var segmentType = ControlPoints[start].Type ?? PathType.Linear; - - foreach (Vector2 t in calculateSubPath(segmentVertices, segmentType)) + // No need to calculate path when there is only 1 vertex + if (segmentVertices.Length == 1) + calculatedPath.Add(segmentVertices[0]); + else if (segmentVertices.Length > 1) { - if (calculatedPath.Count == 0 || calculatedPath.Last() != t) + // Skip the first vertex if it is the same as the last vertex from the previous segment + int skipFirst = calculatedPath.Last() == segmentVertices[0] ? 1 : 0; + foreach (Vector2 t in calculateSubPath(segmentVertices, segmentType).Skip(skipFirst)) calculatedPath.Add(t); } From 0360646e9b8858ef52ab4ad5599a32dbdc3ddbb4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 14:38:53 +0900 Subject: [PATCH 1466/2100] Avoid fast fade out if slider head was not hit --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 09d98654c3..1a6a0a9ecc 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -317,7 +317,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables switch (state) { case ArmedState.Hit: - if (SliderBody?.SnakingOut.Value == true) + if (HeadCircle.IsHit && SliderBody?.SnakingOut.Value == true) Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear. break; } From 4504c9fc43f6783d07041275a40d3fdff29d8f02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 14:42:07 +0900 Subject: [PATCH 1467/2100] Update tests in line with new slider snaking behaviour --- .../TestSceneSliderSnaking.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index 630049f408..aef7dcaa59 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -135,9 +135,9 @@ namespace osu.Game.Rulesets.Osu.Tests } [Test] - public void TestRepeatArrowDoesNotMoveWhenHit() + public void TestRepeatArrowDoesNotMove([Values] bool useAutoplay) { - AddStep("enable autoplay", () => autoplay = true); + AddStep($"set autoplay to {useAutoplay}", () => autoplay = useAutoplay); setSnaking(true); CreateTest(); // repeat might have a chance to update its position depending on where in the frame its hit, @@ -145,15 +145,6 @@ namespace osu.Game.Rulesets.Osu.Tests addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame); } - [Test] - public void TestRepeatArrowMovesWhenNotHit() - { - AddStep("disable autoplay", () => autoplay = false); - setSnaking(true); - CreateTest(); - addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased); - } - private void retrieveSlider(int index) { AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]); From 046e96afcd9ce4c32fcf2a7c1bcaafe9ba811782 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 14:51:03 +0900 Subject: [PATCH 1468/2100] Apply NRT to slider snaking tests --- .../TestSceneSliderSnaking.cs | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index aef7dcaa59..13166c2b6b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -33,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests public partial class TestSceneSliderSnaking : TestSceneOsuPlayer { [Resolved] - private AudioManager audioManager { get; set; } + private AudioManager audioManager { get; set; } = null!; protected override bool Autoplay => autoplay; private bool autoplay; @@ -41,12 +39,12 @@ namespace osu.Game.Rulesets.Osu.Tests private readonly BindableBool snakingIn = new BindableBool(); private readonly BindableBool snakingOut = new BindableBool(); - private IBeatmap beatmap; + private IBeatmap beatmap = null!; private const double duration_of_span = 3605; private const double fade_in_modifier = -1200; - protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) => new ClockBackedTestWorkingBeatmap(this.beatmap = beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); [BackgroundDependencyLoader] @@ -57,15 +55,8 @@ namespace osu.Game.Rulesets.Osu.Tests config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut); } - private Slider slider; - private DrawableSlider drawableSlider; - - [SetUp] - public void Setup() => Schedule(() => - { - slider = null; - drawableSlider = null; - }); + private Slider slider = null!; + private DrawableSlider? drawableSlider; protected override bool HasCustomSteps => true; @@ -150,7 +141,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]); addSeekStep(() => slider.StartTime); AddUntilStep("retrieve drawable slider", () => - (drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null); + (drawableSlider = (DrawableSlider?)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null); } private void addEnsureSnakingInSteps(Func startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionIncreased); @@ -170,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Tests private Func timeAtRepeat(Func startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex; private Func positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? getSliderStart : getSliderEnd; - private List getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; + private List getSliderCurve() => ((PlaySliderBody)drawableSlider!.Body.Drawable).CurrentCurve; private Vector2 getSliderStart() => getSliderCurve().First(); private Vector2 getSliderEnd() => getSliderCurve().Last(); From c0f603eb0e4007cf8a35e8af4e25d9e01441b1ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 15:27:55 +0900 Subject: [PATCH 1469/2100] Fix typo in comment --- osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index 07dc2bea54..f80f43bb77 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -126,7 +126,7 @@ namespace osu.Game.Tournament.Screens.MapPool if (CurrentMatch.Value == null || CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) < 2) return; - // if bans have already been placed, beatmap changes result in a selection being made autoamtically + // if bans have already been placed, beatmap changes result in a selection being made automatically if (beatmap.NewValue?.OnlineID > 0) addForBeatmap(beatmap.NewValue.OnlineID); } From 73db68a49a8226b57c2c41be024fbab946094fd6 Mon Sep 17 00:00:00 2001 From: Magnus-Cosmos Date: Tue, 19 Sep 2023 02:28:28 -0400 Subject: [PATCH 1470/2100] Check if path lists are empty --- osu.Game/Rulesets/Objects/SliderPath.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index cf6d0d212b..0ac057578b 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -261,14 +261,17 @@ namespace osu.Game.Rulesets.Objects // The current vertex ends the segment var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); var segmentType = ControlPoints[start].Type ?? PathType.Linear; + // No need to calculate path when there is only 1 vertex if (segmentVertices.Length == 1) calculatedPath.Add(segmentVertices[0]); else if (segmentVertices.Length > 1) { + List subPath = calculateSubPath(segmentVertices, segmentType); // Skip the first vertex if it is the same as the last vertex from the previous segment - int skipFirst = calculatedPath.Last() == segmentVertices[0] ? 1 : 0; - foreach (Vector2 t in calculateSubPath(segmentVertices, segmentType).Skip(skipFirst)) + int skipFirst = calculatedPath.Count > 0 && subPath.Count > 0 && calculatedPath.Last() == subPath[0] ? 1 : 0; + + foreach (Vector2 t in subPath.Skip(skipFirst)) calculatedPath.Add(t); } From 8e199de78ac57a7d9ed959378eb17df40c7354e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Sep 2023 08:30:17 +0200 Subject: [PATCH 1471/2100] Tweak nano beatmap card UX further to meet expectations --- .../Drawables/Cards/BeatmapCardNano.cs | 2 - .../Cards/CollapsibleButtonContainerSlim.cs | 237 +++++++++++------- 2 files changed, 142 insertions(+), 97 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs index 2f46bc51d6..29f9d7ed2c 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs @@ -149,8 +149,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards }; c.Expanded.BindTarget = Expanded; }); - - Action = () => buttonContainer.TriggerClick(); } private LocalisableString createArtistText() diff --git a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs index d17ff0d759..151c91f4c1 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -17,10 +18,11 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; using osuTK; +using osuTK.Graphics; namespace osu.Game.Beatmaps.Drawables.Cards { - public partial class CollapsibleButtonContainerSlim : OsuClickableContainer + public partial class CollapsibleButtonContainerSlim : Container { public Bindable ShowDetails = new Bindable(); public Bindable FavouriteState = new Bindable(); @@ -56,30 +58,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards protected override Container Content => mainContent; - private readonly APIBeatmapSet beatmapSet; - private readonly Container background; - private readonly Container buttonArea; + private readonly OsuClickableContainer buttonArea; private readonly Container mainArea; private readonly Container mainContent; - private readonly Container icons; - private readonly SpriteIcon downloadIcon; - private readonly LoadingSpinner spinner; - private readonly SpriteIcon goToBeatmapIcon; - private const int icon_size = 12; - private Bindable preferNoVideo = null!; - - [Resolved] - private BeatmapModelDownloader beatmaps { get; set; } = null!; - - [Resolved] - private OsuGame? game { get; set; } - [Resolved] private OsuColour colours { get; set; } = null!; @@ -88,15 +75,13 @@ namespace osu.Game.Beatmaps.Drawables.Cards public CollapsibleButtonContainerSlim(APIBeatmapSet beatmapSet) { - this.beatmapSet = beatmapSet; - downloadTracker = new BeatmapDownloadTracker(beatmapSet); RelativeSizeAxes = Axes.Y; Masking = true; CornerRadius = BeatmapCard.CORNER_RADIUS; - base.Content.AddRange(new Drawable[] + InternalChildren = new Drawable[] { downloadTracker, background = new Container @@ -110,39 +95,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards Colour = Colour4.White }, }, - buttonArea = new Container + buttonArea = new ButtonArea(beatmapSet) { Name = @"Right (button) area", - RelativeSizeAxes = Axes.Y, - Origin = Anchor.TopRight, - Anchor = Anchor.TopRight, - Child = icons = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - downloadIcon = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(icon_size), - Icon = FontAwesome.Solid.Download - }, - spinner = new LoadingSpinner - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(icon_size) - }, - goToBeatmapIcon = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(icon_size), - Icon = FontAwesome.Solid.AngleDoubleRight - }, - } - } + State = { BindTarget = downloadTracker.State } }, mainArea = new Container { @@ -168,23 +124,13 @@ namespace osu.Game.Beatmaps.Drawables.Cards } } } - }); - } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - preferNoVideo = config.GetBindable(OsuSetting.PreferNoVideo); - - downloadIcon.Colour = spinner.Colour = colourProvider.Content1; - goToBeatmapIcon.Colour = colourProvider.Foreground1; + }; } protected override void LoadComplete() { base.LoadComplete(); - preferNoVideo.BindValueChanged(_ => updateState()); downloadTracker.State.BindValueChanged(_ => updateState()); ShowDetails.BindValueChanged(_ => updateState(), true); FinishTransforms(true); @@ -195,51 +141,152 @@ namespace osu.Game.Beatmaps.Drawables.Cards float targetWidth = Width - (ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth); mainArea.ResizeWidthTo(targetWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + background.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + buttonArea.FadeTo(ShowDetails.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + } - var backgroundColour = downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3; - if (ShowDetails.Value) - backgroundColour = backgroundColour.Lighten(0.2f); + private partial class ButtonArea : OsuClickableContainer + { + public Bindable State { get; } = new Bindable(); - background.FadeColour(backgroundColour, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - icons.FadeTo(ShowDetails.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + private readonly APIBeatmapSet beatmapSet; - if (beatmapSet.Availability.DownloadDisabled) + private Box hoverLayer = null!; + private SpriteIcon downloadIcon = null!; + private LoadingSpinner spinner = null!; + private SpriteIcon goToBeatmapIcon = null!; + + private Bindable preferNoVideo = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private BeatmapModelDownloader beatmaps { get; set; } = null!; + + [Resolved] + private OsuGame? game { get; set; } + + public ButtonArea(APIBeatmapSet beatmapSet) { - Enabled.Value = false; - TooltipText = BeatmapsetsStrings.AvailabilityDisabled; - return; + this.beatmapSet = beatmapSet; } - switch (downloadTracker.State.Value) + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) { - case DownloadState.NotDownloaded: - Action = () => beatmaps.Download(beatmapSet, preferNoVideo.Value); - break; + RelativeSizeAxes = Axes.Y; + Origin = Anchor.TopRight; + Anchor = Anchor.TopRight; + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = -BeatmapCard.CORNER_RADIUS }, + Child = hoverLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White.Opacity(0.1f), + Blending = BlendingParameters.Additive + } + }, + downloadIcon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(icon_size), + Icon = FontAwesome.Solid.Download + }, + spinner = new LoadingSpinner + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(icon_size) + }, + goToBeatmapIcon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(icon_size), + Icon = FontAwesome.Solid.AngleDoubleRight + }, + } + }; - case DownloadState.LocallyAvailable: - Action = () => game?.PresentBeatmap(beatmapSet); - break; - - default: - Action = null; - break; + preferNoVideo = config.GetBindable(OsuSetting.PreferNoVideo); } - downloadIcon.FadeTo(downloadTracker.State.Value == DownloadState.NotDownloaded ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - spinner.FadeTo(downloadTracker.State.Value == DownloadState.Downloading || downloadTracker.State.Value == DownloadState.Importing ? 1 : 0, - BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - goToBeatmapIcon.FadeTo(downloadTracker.State.Value == DownloadState.LocallyAvailable ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - - if (downloadTracker.State.Value == DownloadState.NotDownloaded) + protected override void LoadComplete() { - if (!beatmapSet.HasVideo) - TooltipText = BeatmapsetsStrings.PanelDownloadAll; + base.LoadComplete(); + + State.BindValueChanged(_ => updateState(), true); + FinishTransforms(true); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + private void updateState() + { + hoverLayer.FadeTo(IsHovered ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + downloadIcon.FadeTo(State.Value == DownloadState.NotDownloaded ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + downloadIcon.FadeColour(IsHovered ? colourProvider.Content1 : colourProvider.Light1, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + spinner.FadeTo(State.Value == DownloadState.Downloading || State.Value == DownloadState.Importing ? 1 : 0, + BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + spinner.FadeColour(IsHovered ? colourProvider.Content1 : colourProvider.Light1, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + goToBeatmapIcon.FadeTo(State.Value == DownloadState.LocallyAvailable ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + goToBeatmapIcon.FadeColour(IsHovered ? colourProvider.Foreground1 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + switch (State.Value) + { + case DownloadState.NotDownloaded: + Action = () => beatmaps.Download(beatmapSet, preferNoVideo.Value); + break; + + case DownloadState.LocallyAvailable: + Action = () => game?.PresentBeatmap(beatmapSet); + break; + + default: + Action = null; + break; + } + + if (beatmapSet.Availability.DownloadDisabled) + { + Enabled.Value = false; + TooltipText = BeatmapsetsStrings.AvailabilityDisabled; + return; + } + + if (State.Value == DownloadState.NotDownloaded) + { + if (!beatmapSet.HasVideo) + TooltipText = BeatmapsetsStrings.PanelDownloadAll; + else + TooltipText = preferNoVideo.Value ? BeatmapsetsStrings.PanelDownloadNoVideo : BeatmapsetsStrings.PanelDownloadVideo; + } else - TooltipText = preferNoVideo.Value ? BeatmapsetsStrings.PanelDownloadNoVideo : BeatmapsetsStrings.PanelDownloadVideo; - } - else - { - TooltipText = default; + { + TooltipText = default; + } } } } From 0555d22eb8cc5edd3eaf1ab20e63d474bc75194c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 16:35:22 +0900 Subject: [PATCH 1472/2100] Add comment mentioning why hover is disabled on the notification type --- osu.Game/Database/MissingBeatmapNotification.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index d98c07ce1f..f2f7315e8b 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -83,6 +83,7 @@ namespace osu.Game.Database card.Width = Content.DrawWidth; } + // Disable hover so we don't have silly colour conflicts with the nested beatmap card. protected override bool OnHover(HoverEvent e) => false; protected override void OnHoverLost(HoverLostEvent e) { } From 7f30354e61d6d9fa8bc931b894f5040ff6d25c4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 17:20:58 +0900 Subject: [PATCH 1473/2100] Adjust sizing slightly to remove need for `CollapsibleButtonContainerSlim` --- .../Drawables/Cards/BeatmapCardNano.cs | 6 +- .../Cards/CollapsibleButtonContainerSlim.cs | 293 ------------------ .../Database/MissingBeatmapNotification.cs | 4 - 3 files changed, 3 insertions(+), 300 deletions(-) delete mode 100644 osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs index 29f9d7ed2c..4ab2b0c973 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs @@ -38,7 +38,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards [Cached] private readonly BeatmapCardContent content; - private CollapsibleButtonContainerSlim buttonContainer = null!; + private CollapsibleButtonContainer buttonContainer = null!; private FillFlowContainer idleBottomContent = null!; private BeatmapCardDownloadProgressBar downloadProgressBar = null!; @@ -66,12 +66,12 @@ namespace osu.Game.Beatmaps.Drawables.Cards Height = height, Children = new Drawable[] { - buttonContainer = new CollapsibleButtonContainerSlim(BeatmapSet) + buttonContainer = new CollapsibleButtonContainer(BeatmapSet) { Width = Width, FavouriteState = { BindTarget = FavouriteState }, ButtonsCollapsedWidth = 5, - ButtonsExpandedWidth = 20, + ButtonsExpandedWidth = 30, Children = new Drawable[] { new FillFlowContainer diff --git a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs deleted file mode 100644 index 151c91f4c1..0000000000 --- a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs +++ /dev/null @@ -1,293 +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 osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; -using osu.Game.Configuration; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Overlays; -using osu.Game.Resources.Localisation.Web; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Beatmaps.Drawables.Cards -{ - public partial class CollapsibleButtonContainerSlim : Container - { - public Bindable ShowDetails = new Bindable(); - public Bindable FavouriteState = new Bindable(); - - private readonly BeatmapDownloadTracker downloadTracker; - - private float buttonsExpandedWidth; - - public float ButtonsExpandedWidth - { - get => buttonsExpandedWidth; - set - { - buttonsExpandedWidth = value; - buttonArea.Width = value; - if (IsLoaded) - updateState(); - } - } - - private float buttonsCollapsedWidth; - - public float ButtonsCollapsedWidth - { - get => buttonsCollapsedWidth; - set - { - buttonsCollapsedWidth = value; - if (IsLoaded) - updateState(); - } - } - - protected override Container Content => mainContent; - - private readonly Container background; - - private readonly OsuClickableContainer buttonArea; - - private readonly Container mainArea; - private readonly Container mainContent; - - private const int icon_size = 12; - - [Resolved] - private OsuColour colours { get; set; } = null!; - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; - - public CollapsibleButtonContainerSlim(APIBeatmapSet beatmapSet) - { - downloadTracker = new BeatmapDownloadTracker(beatmapSet); - - RelativeSizeAxes = Axes.Y; - Masking = true; - CornerRadius = BeatmapCard.CORNER_RADIUS; - - InternalChildren = new Drawable[] - { - downloadTracker, - background = new Container - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.White - }, - }, - buttonArea = new ButtonArea(beatmapSet) - { - Name = @"Right (button) area", - State = { BindTarget = downloadTracker.State } - }, - mainArea = new Container - { - Name = @"Main content", - RelativeSizeAxes = Axes.Y, - CornerRadius = BeatmapCard.CORNER_RADIUS, - Masking = true, - Children = new Drawable[] - { - new BeatmapCardContentBackground(beatmapSet) - { - RelativeSizeAxes = Axes.Both, - Dimmed = { BindTarget = ShowDetails } - }, - mainContent = new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Horizontal = 10, - Vertical = 4 - }, - } - } - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - downloadTracker.State.BindValueChanged(_ => updateState()); - ShowDetails.BindValueChanged(_ => updateState(), true); - FinishTransforms(true); - } - - private void updateState() - { - float targetWidth = Width - (ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth); - - mainArea.ResizeWidthTo(targetWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - background.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - buttonArea.FadeTo(ShowDetails.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - } - - private partial class ButtonArea : OsuClickableContainer - { - public Bindable State { get; } = new Bindable(); - - private readonly APIBeatmapSet beatmapSet; - - private Box hoverLayer = null!; - private SpriteIcon downloadIcon = null!; - private LoadingSpinner spinner = null!; - private SpriteIcon goToBeatmapIcon = null!; - - private Bindable preferNoVideo = null!; - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; - - [Resolved] - private BeatmapModelDownloader beatmaps { get; set; } = null!; - - [Resolved] - private OsuGame? game { get; set; } - - public ButtonArea(APIBeatmapSet beatmapSet) - { - this.beatmapSet = beatmapSet; - } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - RelativeSizeAxes = Axes.Y; - Origin = Anchor.TopRight; - Anchor = Anchor.TopRight; - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = -BeatmapCard.CORNER_RADIUS }, - Child = hoverLayer = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.White.Opacity(0.1f), - Blending = BlendingParameters.Additive - } - }, - downloadIcon = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(icon_size), - Icon = FontAwesome.Solid.Download - }, - spinner = new LoadingSpinner - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(icon_size) - }, - goToBeatmapIcon = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(icon_size), - Icon = FontAwesome.Solid.AngleDoubleRight - }, - } - }; - - preferNoVideo = config.GetBindable(OsuSetting.PreferNoVideo); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - State.BindValueChanged(_ => updateState(), true); - FinishTransforms(true); - } - - protected override bool OnHover(HoverEvent e) - { - updateState(); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateState(); - base.OnHoverLost(e); - } - - private void updateState() - { - hoverLayer.FadeTo(IsHovered ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - - downloadIcon.FadeTo(State.Value == DownloadState.NotDownloaded ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - downloadIcon.FadeColour(IsHovered ? colourProvider.Content1 : colourProvider.Light1, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - - spinner.FadeTo(State.Value == DownloadState.Downloading || State.Value == DownloadState.Importing ? 1 : 0, - BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - spinner.FadeColour(IsHovered ? colourProvider.Content1 : colourProvider.Light1, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - - goToBeatmapIcon.FadeTo(State.Value == DownloadState.LocallyAvailable ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - goToBeatmapIcon.FadeColour(IsHovered ? colourProvider.Foreground1 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - - switch (State.Value) - { - case DownloadState.NotDownloaded: - Action = () => beatmaps.Download(beatmapSet, preferNoVideo.Value); - break; - - case DownloadState.LocallyAvailable: - Action = () => game?.PresentBeatmap(beatmapSet); - break; - - default: - Action = null; - break; - } - - if (beatmapSet.Availability.DownloadDisabled) - { - Enabled.Value = false; - TooltipText = BeatmapsetsStrings.AvailabilityDisabled; - return; - } - - if (State.Value == DownloadState.NotDownloaded) - { - if (!beatmapSet.HasVideo) - TooltipText = BeatmapsetsStrings.PanelDownloadAll; - else - TooltipText = preferNoVideo.Value ? BeatmapsetsStrings.PanelDownloadNoVideo : BeatmapsetsStrings.PanelDownloadVideo; - } - else - { - TooltipText = default; - } - } - } - } -} diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index f2f7315e8b..bc96625ead 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -83,10 +83,6 @@ namespace osu.Game.Database card.Width = Content.DrawWidth; } - // Disable hover so we don't have silly colour conflicts with the nested beatmap card. - protected override bool OnHover(HoverEvent e) => false; - protected override void OnHoverLost(HoverLostEvent e) { } - private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) { if (changes?.InsertedIndices == null) return; From 62f97a8d83a5baa59da76cdcea179b6be500360a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Sep 2023 10:27:24 +0200 Subject: [PATCH 1474/2100] Adjust beatmap card thumbnail dim state to match web better --- .../Drawables/Cards/BeatmapCardThumbnail.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs index ad91615031..5a26a988fb 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -3,15 +3,15 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.Drawables.Cards.Buttons; -using osu.Game.Graphics; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Framework.Graphics.UserInterface; using osuTK; -using osuTK.Graphics; namespace osu.Game.Beatmaps.Drawables.Cards { @@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards set => foreground.Padding = value; } - private readonly UpdateableOnlineBeatmapSetCover cover; + private readonly Box background; private readonly Container foreground; private readonly PlayButton playButton; private readonly CircularProgress progress; @@ -33,15 +33,22 @@ namespace osu.Game.Beatmaps.Drawables.Cards protected override Container Content => content; + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + public BeatmapCardThumbnail(APIBeatmapSet beatmapSetInfo) { InternalChildren = new Drawable[] { - cover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) + new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) { RelativeSizeAxes = Axes.Both, OnlineInfo = beatmapSetInfo }, + background = new Box + { + RelativeSizeAxes = Axes.Both + }, foreground = new Container { RelativeSizeAxes = Axes.Both, @@ -68,7 +75,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load() { progress.Colour = colourProvider.Highlight1; } @@ -89,7 +96,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards bool shouldDim = Dimmed.Value || playButton.Playing.Value; playButton.FadeTo(shouldDim ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - cover.FadeColour(shouldDim ? OsuColour.Gray(0.2f) : Color4.White, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + background.FadeColour(colourProvider.Background6.Opacity(shouldDim ? 0.8f : 0f), BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); } } } From 0593c76c57436757e7da3ada6556c683396fdbfa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 17:34:24 +0900 Subject: [PATCH 1475/2100] Fix log output using incorrect name --- osu.Game/Scoring/ScoreImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 2875035e1b..26594fb815 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -56,7 +56,7 @@ namespace osu.Game.Scoring catch (LegacyScoreDecoder.BeatmapNotFoundException e) { onMissingBeatmap(e, archive, name); - Logger.Log($@"Score '{name}' failed to import: no corresponding beatmap with the hash '{e.Hash}' could be found.", LoggingTarget.Database); + Logger.Log($@"Score '{archive.Name}' failed to import: no corresponding beatmap with the hash '{e.Hash}' could be found.", LoggingTarget.Database); return null; } } From f726c38215b6ff35305969b7c7e79f89a25818f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 17:41:00 +0900 Subject: [PATCH 1476/2100] Pass `ArchiveReader` instead of `Stream` to simplify resolution code --- .../TestSceneMissingBeatmapNotification.cs | 4 +-- .../Database/MissingBeatmapNotification.cs | 14 ++++----- osu.Game/Scoring/ScoreImporter.cs | 30 ++++--------------- 3 files changed, 14 insertions(+), 34 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMissingBeatmapNotification.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMissingBeatmapNotification.cs index 23b9c5f76a..f5506edf3b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneMissingBeatmapNotification.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneMissingBeatmapNotification.cs @@ -1,7 +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.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -9,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Database; using osu.Game.Overlays; +using osu.Game.Tests.Scores.IO; namespace osu.Game.Tests.Visual.UserInterface { @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.UserInterface AutoSizeAxes = Axes.Y, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Child = new MissingBeatmapNotification(CreateAPIBeatmapSet(Ruleset.Value).Beatmaps.First(), new MemoryStream(), "deadbeef") + Child = new MissingBeatmapNotification(CreateAPIBeatmapSet(Ruleset.Value).Beatmaps.First(), new ImportScoreTest.TestArchiveReader(), "deadbeef") }; } } diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index bc96625ead..261de2a938 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -2,19 +2,18 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.IO; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Configuration; +using osu.Game.IO.Archives; +using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Notifications; using osu.Game.Scoring; using Realms; -using osu.Game.Localisation; namespace osu.Game.Database { @@ -29,7 +28,7 @@ namespace osu.Game.Database [Resolved] private RealmAccess realm { get; set; } = null!; - private readonly MemoryStream scoreStream; + private readonly ArchiveReader scoreArchive; private readonly APIBeatmapSet beatmapSetInfo; private readonly string beatmapHash; @@ -39,12 +38,12 @@ namespace osu.Game.Database private IDisposable? realmSubscription; - public MissingBeatmapNotification(APIBeatmap beatmap, MemoryStream scoreStream, string beatmapHash) + public MissingBeatmapNotification(APIBeatmap beatmap, ArchiveReader scoreArchive, string beatmapHash) { beatmapSetInfo = beatmap.BeatmapSet!; this.beatmapHash = beatmapHash; - this.scoreStream = scoreStream; + this.scoreArchive = scoreArchive; } [BackgroundDependencyLoader] @@ -89,7 +88,8 @@ namespace osu.Game.Database if (sender.Any(s => s.Beatmaps.Any(b => b.MD5Hash == beatmapHash))) { - var importTask = new ImportTask(scoreStream, "score.osr"); + string name = scoreArchive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase)); + var importTask = new ImportTask(scoreArchive.GetStream(name), name); scoreManager.Import(new[] { importTask }); realmSubscription?.Dispose(); Close(false); diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 26594fb815..b85b6a066e 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Threading; using Newtonsoft.Json; @@ -55,36 +54,17 @@ namespace osu.Game.Scoring } catch (LegacyScoreDecoder.BeatmapNotFoundException e) { - onMissingBeatmap(e, archive, name); Logger.Log($@"Score '{archive.Name}' failed to import: no corresponding beatmap with the hash '{e.Hash}' could be found.", LoggingTarget.Database); + + // In the case of a missing beatmap, let's attempt to resolve it and show a prompt to the user to download the required beatmap. + var req = new GetBeatmapRequest(new BeatmapInfo { MD5Hash = e.Hash }); + req.Success += res => PostNotification?.Invoke(new MissingBeatmapNotification(res, archive, e.Hash)); + api.Queue(req); return null; } } } - private void onMissingBeatmap(LegacyScoreDecoder.BeatmapNotFoundException e, ArchiveReader archive, string name) - { - var stream = new MemoryStream(); - - // stream will be closed after the exception was thrown, so fetch the stream again. - using (var scoreStream = archive.GetStream(name)) - { - scoreStream.CopyTo(stream); - } - - var req = new GetBeatmapRequest(new BeatmapInfo - { - MD5Hash = e.Hash - }); - - req.Success += res => - { - PostNotification?.Invoke(new MissingBeatmapNotification(res, stream, e.Hash)); - }; - - api.Queue(req); - } - public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); protected override void Populate(ScoreInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) From cdb5fea513f06bf18eefc62a8b0d4997d4c8a91d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 17:53:00 +0900 Subject: [PATCH 1477/2100] Remove unused translations --- .../Localisation/OnlineSettingsStrings.cs | 5 ----- osu.Game/Localisation/WebSettingsStrings.cs | 19 ------------------- 2 files changed, 24 deletions(-) delete mode 100644 osu.Game/Localisation/WebSettingsStrings.cs diff --git a/osu.Game/Localisation/OnlineSettingsStrings.cs b/osu.Game/Localisation/OnlineSettingsStrings.cs index 5ea53a13bf..0660bac172 100644 --- a/osu.Game/Localisation/OnlineSettingsStrings.cs +++ b/osu.Game/Localisation/OnlineSettingsStrings.cs @@ -54,11 +54,6 @@ namespace osu.Game.Localisation ///

yftJ!Y0GsdcN%;MEM+5u>2KjHnOqx_AeVsl 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 literal 87032 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 CAgN>k diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay-1@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay-1@2x.png index e611add3ba9861baf68614ec51d63d3f94c8592a..3b5e886933d2f7a8803a44304c5439058ef86fa2 100755 GIT binary patch literal 40858 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�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 U^GtX>4gf!XyL%uw1Pc(DKu92j;1Zk!4M7q# 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

public static LocalisableString PreferNoVideo => new TranslatableString(getKey(@"prefer_no_video"), @"Prefer downloads without video"); - /// - /// "Automatically download beatmaps when spectating" - /// - public static LocalisableString AutomaticallyDownloadWhenSpectating => new TranslatableString(getKey(@"automatically_download_when_spectating"), @"Automatically download beatmaps when spectating"); - /// /// "Automatically download missing beatmaps" /// diff --git a/osu.Game/Localisation/WebSettingsStrings.cs b/osu.Game/Localisation/WebSettingsStrings.cs deleted file mode 100644 index b3033524dc..0000000000 --- a/osu.Game/Localisation/WebSettingsStrings.cs +++ /dev/null @@ -1,19 +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 osu.Framework.Localisation; - -namespace osu.Game.Localisation -{ - public static class WebSettingsStrings - { - private const string prefix = @"osu.Game.Resources.Localisation.WebSettings"; - - /// - /// "Automatically download missing beatmaps" - /// - public static LocalisableString AutomaticallyDownloadMissingBeatmaps => new TranslatableString(getKey(@"automatically_download_missing_beatmaps"), @"Automatically download missing beatmaps"); - - private static string getKey(string key) => $@"{prefix}:{key}"; - } -} \ No newline at end of file From 05e05f8160a73dd9f270147874ba0632897d2670 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 18:02:08 +0900 Subject: [PATCH 1478/2100] Increase transition speed slightly --- osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index a16f6d5689..25e42bcbf7 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -19,7 +19,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { public abstract partial class BeatmapCard : OsuClickableContainer, IHasContextMenu { - public const float TRANSITION_DURATION = 400; + public const float TRANSITION_DURATION = 340; public const float CORNER_RADIUS = 10; protected const float WIDTH = 430; From ed9039f60f31415089be387e9075e6439099ee58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Sep 2023 11:09:23 +0200 Subject: [PATCH 1479/2100] Fix notification text sets overwriting each other --- .../Database/MissingBeatmapNotification.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index 261de2a938..584b2675f3 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -52,14 +52,6 @@ namespace osu.Game.Database realmSubscription = realm.RegisterForNotifications( realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); - realm.Run(r => - { - if (r.All().Any(s => !s.DeletePending && s.OnlineID == beatmapSetInfo.OnlineID)) - { - Text = NotificationsStrings.MismatchingBeatmapForReplay; - } - }); - autoDownloadConfig = config.GetBindable(OsuSetting.AutomaticallyDownloadMissingBeatmaps); noVideoSetting = config.GetBindable(OsuSetting.PreferNoVideo); @@ -71,9 +63,15 @@ namespace osu.Game.Database base.LoadComplete(); if (autoDownloadConfig.Value) + { + Text = NotificationsStrings.DownloadingBeatmapForReplay; beatmapDownloader.Download(beatmapSetInfo, noVideoSetting.Value); - - Text = autoDownloadConfig.Value ? NotificationsStrings.DownloadingBeatmapForReplay : NotificationsStrings.MissingBeatmapForReplay; + } + else + { + bool missingSetMatchesExistingOnlineId = realm.Run(r => r.All().Any(s => !s.DeletePending && s.OnlineID == beatmapSetInfo.OnlineID)); + Text = missingSetMatchesExistingOnlineId ? NotificationsStrings.MismatchingBeatmapForReplay : NotificationsStrings.MissingBeatmapForReplay; + } } protected override void Update() From 773ec469898c9d616c2211723ebb3a9742dc76c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Sep 2023 23:59:44 +0900 Subject: [PATCH 1480/2100] Expose some storyboard pieces to allow better testability --- osu.Game/Storyboards/Drawables/DrawableStoryboard.cs | 4 +++- osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs | 5 ++++- osu.Game/Storyboards/Storyboard.cs | 2 +- osu.Game/Storyboards/StoryboardVideo.cs | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 6931cea81e..a11251ed22 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -23,7 +23,9 @@ namespace osu.Game.Storyboards.Drawables { public partial class DrawableStoryboard : Container { - [Cached] + public Vector2 AppliedScale { get; private set; } + + [Cached(typeof(Storyboard))] public Storyboard Storyboard { get; } /// diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs index 38e7ff1c70..40842fe7ed 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.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.Threading; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -30,10 +31,12 @@ namespace osu.Game.Storyboards.Drawables InternalChild = ElementContainer = new LayerElementContainer(layer); } - protected partial class LayerElementContainer : LifetimeManagementContainer + public partial class LayerElementContainer : LifetimeManagementContainer { private readonly StoryboardLayer storyboardLayer; + public IEnumerable Elements => InternalChildren; + public LayerElementContainer(StoryboardLayer layer) { storyboardLayer = layer; diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 1892855d3d..03e30d6272 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -86,7 +86,7 @@ namespace osu.Game.Storyboards } } - public DrawableStoryboard CreateDrawable(IReadOnlyList? mods = null) => + public virtual DrawableStoryboard CreateDrawable(IReadOnlyList? mods = null) => new DrawableStoryboard(this, mods); private static readonly string[] image_extensions = { @".png", @".jpg" }; diff --git a/osu.Game/Storyboards/StoryboardVideo.cs b/osu.Game/Storyboards/StoryboardVideo.cs index 4652e45852..8c11e19a06 100644 --- a/osu.Game/Storyboards/StoryboardVideo.cs +++ b/osu.Game/Storyboards/StoryboardVideo.cs @@ -14,7 +14,7 @@ namespace osu.Game.Storyboards public double StartTime { get; } - public StoryboardVideo(string path, int offset) + public StoryboardVideo(string path, double offset) { Path = path; StartTime = offset; From 2f020f8682aece5fcf4b54f879c4492149485ff6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 19:44:41 +0900 Subject: [PATCH 1481/2100] Add test coverage of storyboard preferring skin when specified --- .../TestSceneDrawableStoryboardSprite.cs | 176 +++++++++++++++--- 1 file changed, 150 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs index ec4bb1a86b..d20c7c2f7a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs @@ -2,17 +2,23 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Storyboards; using osu.Game.Storyboards.Drawables; +using osu.Game.Tests.Resources; using osuTK; namespace osu.Game.Tests.Visual.Gameplay @@ -21,17 +27,21 @@ namespace osu.Game.Tests.Visual.Gameplay { protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); - [Cached] - private Storyboard storyboard { get; set; } = new Storyboard(); + [Cached(typeof(Storyboard))] + private TestStoryboard storyboard { get; set; } = new TestStoryboard(); private IEnumerable sprites => this.ChildrenOfType(); + private const string lookup_name = "hitcircleoverlay"; + [Test] public void TestSkinSpriteDisallowedByDefault() { - const string lookup_name = "hitcircleoverlay"; - - AddStep("allow skin lookup", () => storyboard.UseSkinSprites = false); + AddStep("disallow all lookups", () => + { + storyboard.UseSkinSprites = false; + storyboard.AlwaysProvideTexture = false; + }); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); @@ -40,11 +50,13 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestAllowLookupFromSkin() + public void TestLookupFromStoryboard() { - const string lookup_name = "hitcircleoverlay"; - - AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true); + AddStep("allow storyboard lookup", () => + { + storyboard.UseSkinSprites = false; + storyboard.AlwaysProvideTexture = true; + }); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); @@ -52,16 +64,54 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("sprite found texture", () => sprites.Any(sprite => sprite.ChildrenOfType().All(s => s.Texture != null))); - AddAssert("skinnable sprite has correct size", () => - sprites.Any(sprite => sprite.ChildrenOfType().All(s => s.Size == new Vector2(128)))); + assertStoryboardSourced(); + } + + [Test] + public void TestSkinLookupPreferredOverStoryboard() + { + AddStep("allow all lookups", () => + { + storyboard.UseSkinSprites = true; + storyboard.AlwaysProvideTexture = true; + }); + + AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); + + // Only checking for at least one sprite that succeeded, as not all skins in this test provide the hitcircleoverlay texture. + AddAssert("sprite found texture", () => + sprites.Any(sprite => sprite.ChildrenOfType().All(s => s.Texture != null))); + + assertSkinSourced(); + } + + [Test] + public void TestAllowLookupFromSkin() + { + AddStep("allow skin lookup", () => + { + storyboard.UseSkinSprites = true; + storyboard.AlwaysProvideTexture = false; + }); + + AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); + + // Only checking for at least one sprite that succeeded, as not all skins in this test provide the hitcircleoverlay texture. + AddAssert("sprite found texture", () => + sprites.Any(sprite => sprite.ChildrenOfType().All(s => s.Texture != null))); + + assertSkinSourced(); } [Test] public void TestFlippedSprite() { - const string lookup_name = "hitcircleoverlay"; + AddStep("allow all lookups", () => + { + storyboard.UseSkinSprites = true; + storyboard.AlwaysProvideTexture = true; + }); - AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddStep("flip sprites", () => sprites.ForEach(s => { @@ -74,9 +124,12 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestZeroScale() { - const string lookup_name = "hitcircleoverlay"; + AddStep("allow all lookups", () => + { + storyboard.UseSkinSprites = true; + storyboard.AlwaysProvideTexture = true; + }); - AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddAssert("sprites present", () => sprites.All(s => s.IsPresent)); AddStep("scale sprite", () => sprites.ForEach(s => s.VectorScale = new Vector2(0, 1))); @@ -86,9 +139,12 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestNegativeScale() { - const string lookup_name = "hitcircleoverlay"; + AddStep("allow all lookups", () => + { + storyboard.UseSkinSprites = true; + storyboard.AlwaysProvideTexture = true; + }); - AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddStep("scale sprite", () => sprites.ForEach(s => s.VectorScale = new Vector2(-1))); AddAssert("origin flipped", () => sprites.All(s => s.Origin == Anchor.BottomRight)); @@ -97,9 +153,12 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestNegativeScaleWithFlippedSprite() { - const string lookup_name = "hitcircleoverlay"; + AddStep("allow all lookups", () => + { + storyboard.UseSkinSprites = true; + storyboard.AlwaysProvideTexture = true; + }); - AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddStep("scale sprite", () => sprites.ForEach(s => s.VectorScale = new Vector2(-1))); AddAssert("origin flipped", () => sprites.All(s => s.Origin == Anchor.BottomRight)); @@ -111,13 +170,78 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("origin back", () => sprites.All(s => s.Origin == Anchor.TopLeft)); } - private DrawableStoryboardSprite createSprite(string lookupName, Anchor origin, Vector2 initialPosition) - => new DrawableStoryboardSprite( - new StoryboardSprite(lookupName, origin, initialPosition) - ).With(s => + private DrawableStoryboard createSprite(string lookupName, Anchor origin, Vector2 initialPosition) + { + var layer = storyboard.GetLayer("Background"); + + var sprite = new StoryboardSprite(lookupName, origin, initialPosition); + sprite.AddLoop(Time.Current, 100).Alpha.Add(Easing.None, 0, 10000, 1, 1); + + layer.Elements.Clear(); + layer.Add(sprite); + + return storyboard.CreateDrawable(); + } + + private void assertStoryboardSourced() + { + AddAssert("sprite came from storyboard", () => + sprites.Any(sprite => sprite.ChildrenOfType().All(s => s.Size == new Vector2(200)))); + } + + private void assertSkinSourced() + { + AddAssert("sprite came from skin", () => + sprites.Any(sprite => sprite.ChildrenOfType().All(s => s.Size == new Vector2(128)))); + } + + private partial class TestStoryboard : Storyboard + { + public override DrawableStoryboard CreateDrawable(IReadOnlyList? mods = null) { - s.LifetimeStart = double.MinValue; - s.LifetimeEnd = double.MaxValue; - }); + return new TestDrawableStoryboard(this, mods); + } + + public bool AlwaysProvideTexture { get; set; } + + public override string GetStoragePathFromStoryboardPath(string path) => AlwaysProvideTexture ? path : string.Empty; + + private partial class TestDrawableStoryboard : DrawableStoryboard + { + private readonly bool alwaysProvideTexture; + + public TestDrawableStoryboard(TestStoryboard storyboard, IReadOnlyList? mods) + : base(storyboard, mods) + { + alwaysProvideTexture = storyboard.AlwaysProvideTexture; + } + + protected override IResourceStore CreateResourceLookupStore() => alwaysProvideTexture + ? new AlwaysReturnsTextureStore() + : new ResourceStore(); + + internal class AlwaysReturnsTextureStore : IResourceStore + { + private const string test_image = "Resources/Textures/test-image.png"; + + private readonly DllResourceStore store; + + public AlwaysReturnsTextureStore() + { + store = TestResources.GetStore(); + } + + public void Dispose() => store.Dispose(); + + public byte[] Get(string name) => store.Get(test_image); + + public Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => store.GetAsync(test_image, cancellationToken); + + public Stream GetStream(string name) => store.GetStream(test_image); + + public IEnumerable GetAvailableResources() => store.GetAvailableResources(); + } + } + } } } From 4a7dc4d7927aaba2b1f527e6b2939fa179189535 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 19:13:58 +0900 Subject: [PATCH 1482/2100] Fix storyboard `UseSkinSprites` being implemented incorrectly This was implemented as a "fallback", but it's actually intended to be an "override". As in it allows storyboarders to *prefer* a skin sprite before falling back to a local version contained within the storyboard. Can be tested with https://osu.ppy.sh/beatmapsets/832364#osu/1743837. Closes https://github.com/ppy/osu/issues/24813. --- .../Drawables/DrawableStoryboardSprite.cs | 23 ++++++++++++------- osu.Game/Storyboards/Storyboard.cs | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 379de1a497..14132654d1 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.ComponentModel; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -74,6 +75,15 @@ namespace osu.Game.Storyboards.Drawables public override bool IsPresent => !float.IsNaN(DrawPosition.X) && !float.IsNaN(DrawPosition.Y) && base.IsPresent; + [Resolved] + private ISkinSource skin { get; set; } = null!; + + [Resolved] + private Storyboard storyboard { get; set; } = null!; + + [Resolved] + private TextureStore textureStore { get; set; } = null!; + public DrawableStoryboardSprite(StoryboardSprite sprite) { Sprite = sprite; @@ -84,24 +94,21 @@ namespace osu.Game.Storyboards.Drawables LifetimeEnd = sprite.EndTimeForDisplay; } - [Resolved] - private ISkinSource skin { get; set; } = null!; - [BackgroundDependencyLoader] - private void load(TextureStore textureStore, Storyboard storyboard) + private void load() { - Texture = textureStore.Get(Sprite.Path); - - if (Texture == null && storyboard.UseSkinSprites) + if (storyboard.UseSkinSprites) { skin.SourceChanged += skinSourceChanged; skinSourceChanged(); } + else + Texture = textureStore.Get(Sprite.Path); Sprite.ApplyTransforms(this); } - private void skinSourceChanged() => Texture = skin.GetTexture(Sprite.Path); + private void skinSourceChanged() => Texture = skin.GetTexture(Sprite.Path) ?? textureStore.Get(Sprite.Path); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 03e30d6272..21342831b0 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -18,7 +18,7 @@ namespace osu.Game.Storyboards public BeatmapInfo BeatmapInfo = new BeatmapInfo(); /// - /// Whether the storyboard can fall back to skin sprites in case no matching storyboard sprites are found. + /// Whether the storyboard should prefer textures from the current skin before using local storyboard textures. /// public bool UseSkinSprites { get; set; } From 320a9fc17169c81b082c42d16fb750b9fc7e6026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Sep 2023 13:47:46 +0200 Subject: [PATCH 1483/2100] Replace test with better test --- .../Formats/LegacyBeatmapDecoderTest.cs | 29 +++++++++++-------- osu.Game.Tests/Resources/invalid-bank.osu | 18 ++++++++---- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 6fe9c902bb..1ba63f4037 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -622,7 +622,7 @@ namespace osu.Game.Tests.Beatmaps.Formats } [Test] - public void TestInvalidBankDefaultsToNone() + public void TestInvalidBankDefaultsToNormal() { var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; @@ -631,20 +631,25 @@ namespace osu.Game.Tests.Beatmaps.Formats { var hitObjects = decoder.Decode(stream).HitObjects; - Assert.AreEqual(HitSampleInfo.BANK_NORMAL, hitObjects[0].Samples[0].Bank); - Assert.AreEqual(HitSampleInfo.BANK_NORMAL, hitObjects[0].Samples[1].Bank); + assertObjectHasBanks(hitObjects[0], HitSampleInfo.BANK_DRUM); + assertObjectHasBanks(hitObjects[1], HitSampleInfo.BANK_NORMAL); + assertObjectHasBanks(hitObjects[2], HitSampleInfo.BANK_SOFT); + assertObjectHasBanks(hitObjects[3], HitSampleInfo.BANK_DRUM); + assertObjectHasBanks(hitObjects[4], HitSampleInfo.BANK_NORMAL); - Assert.AreEqual(HitSampleInfo.BANK_NORMAL, hitObjects[1].Samples[0].Bank); - Assert.AreEqual(HitSampleInfo.BANK_SOFT, hitObjects[1].Samples[1].Bank); + assertObjectHasBanks(hitObjects[5], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_DRUM); + assertObjectHasBanks(hitObjects[6], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_NORMAL); + assertObjectHasBanks(hitObjects[7], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_SOFT); + assertObjectHasBanks(hitObjects[8], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_DRUM); + assertObjectHasBanks(hitObjects[9], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_NORMAL); + } - Assert.AreEqual(HitSampleInfo.BANK_SOFT, hitObjects[2].Samples[0].Bank); - Assert.AreEqual(HitSampleInfo.BANK_SOFT, hitObjects[2].Samples[1].Bank); + void assertObjectHasBanks(HitObject hitObject, string normalBank, string? additionsBank = null) + { + Assert.AreEqual(normalBank, hitObject.Samples[0].Bank); - Assert.AreEqual(HitSampleInfo.BANK_NORMAL, hitObjects[3].Samples[0].Bank); - Assert.AreEqual(HitSampleInfo.BANK_SOFT, hitObjects[3].Samples[1].Bank); - - Assert.AreEqual(HitSampleInfo.BANK_NORMAL, hitObjects[4].Samples[0].Bank); - Assert.AreEqual(HitSampleInfo.BANK_NORMAL, hitObjects[4].Samples[1].Bank); + if (additionsBank != null) + Assert.AreEqual(additionsBank, hitObject.Samples[1].Bank); } } diff --git a/osu.Game.Tests/Resources/invalid-bank.osu b/osu.Game.Tests/Resources/invalid-bank.osu index fb54a61fd3..8c554cc17f 100644 --- a/osu.Game.Tests/Resources/invalid-bank.osu +++ b/osu.Game.Tests/Resources/invalid-bank.osu @@ -3,9 +3,17 @@ osu file format v14 [General] SampleSet: Normal +[TimingPoints] +0,500,4,3,0,100,1,0 + [HitObjects] -256,192,1000,1,8,0:0:0:0: -256,192,2000,1,8,1:2:0:0: -256,192,3000,1,8,2:62:0:0: -256,192,4000,1,8,41:2:0:0: -256,192,5000,1,8,41:62:0:0: +256,192,1000,5,0,0:0:0:0: +256,192,2000,1,0,1:0:0:0: +256,192,3000,1,0,2:0:0:0: +256,192,4000,1,0,3:0:0:0: +256,192,5000,1,0,42:0:0:0: +256,192,6000,5,4,0:0:0:0: +256,192,7000,1,4,0:1:0:0: +256,192,8000,1,4,0:2:0:0: +256,192,9000,1,4,0:3:0:0: +256,192,10000,1,4,0:42:0:0: From c4a0ca326ed5e02a9219dee579cb9a01c554960b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Sep 2023 13:53:49 +0200 Subject: [PATCH 1484/2100] Replace sample bank fix with more correct fix stable does not treat unknown enum members as `None` / `Auto`, it treats them as `Normal`: switch (sampleSet) { case SampleSet.Normal: default: sample = 0; break; case SampleSet.None: case SampleSet.Soft: sample = 1; break; case SampleSet.Drum: sample = 2; break; } (from https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/Audio/AudioEngine.cs#L1158-L1171). --- .../Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 339e9bb5bc..d20f2d31bb 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -190,13 +190,18 @@ namespace osu.Game.Rulesets.Objects.Legacy string[] split = str.Split(':'); var bank = (LegacySampleBank)Parsing.ParseInt(split[0]); + if (!Enum.IsDefined(bank)) + bank = LegacySampleBank.Normal; + var addBank = (LegacySampleBank)Parsing.ParseInt(split[1]); + if (!Enum.IsDefined(addBank)) + addBank = LegacySampleBank.Normal; string stringBank = bank.ToString().ToLowerInvariant(); - if (stringBank == @"none" || !Enum.IsDefined(bank)) + if (stringBank == @"none") stringBank = null; string stringAddBank = addBank.ToString().ToLowerInvariant(); - if (stringAddBank == @"none" || !Enum.IsDefined(addBank)) + if (stringAddBank == @"none") stringAddBank = null; bankInfo.BankForNormal = stringBank; From ba518e1da8442d8aaf17a235d227a394f077689e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Sep 2023 20:11:16 +0200 Subject: [PATCH 1485/2100] Fix `StoryboardResourceLookupStore` dying on failure to unmap path Before the introduction of `StoryboardResourceLookupStore`, missing files would softly fail by use of null fallbacks. After the aforementioned class was added, however, the fallbacks would not work anymore if for whatever reason `GetStoragePathFromStoryboardPath()` failed to unmap the storyboard asset name to a storage path. --- .../Drawables/DrawableStoryboard.cs | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 6931cea81e..c2a58d46ef 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -142,14 +142,32 @@ namespace osu.Game.Storyboards.Drawables public void Dispose() => realmFileStore.Dispose(); - public byte[] Get(string name) => - realmFileStore.Get(storyboard.GetStoragePathFromStoryboardPath(name)); + public byte[] Get(string name) + { + string? storagePath = storyboard.GetStoragePathFromStoryboardPath(name); - public Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => - realmFileStore.GetAsync(storyboard.GetStoragePathFromStoryboardPath(name), cancellationToken); + return string.IsNullOrEmpty(storagePath) + ? null! + : realmFileStore.Get(storagePath); + } - public Stream GetStream(string name) => - realmFileStore.GetStream(storyboard.GetStoragePathFromStoryboardPath(name)); + public Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) + { + string? storagePath = storyboard.GetStoragePathFromStoryboardPath(name); + + return string.IsNullOrEmpty(storagePath) + ? Task.FromResult(null!) + : realmFileStore.GetAsync(storagePath, cancellationToken); + } + + public Stream? GetStream(string name) + { + string? storagePath = storyboard.GetStoragePathFromStoryboardPath(name); + + return string.IsNullOrEmpty(storagePath) + ? null + : realmFileStore.GetStream(storagePath); + } public IEnumerable GetAvailableResources() => realmFileStore.GetAvailableResources(); From 641e651bf282aeeb11050756e63e3a98cc136bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Sep 2023 20:18:33 +0200 Subject: [PATCH 1486/2100] Fix `DrawableStoryboardVideo` attempting to unmap path once too much The `StoryboardResourceLookupStore` cached at storyboard level is supposed to already be handling that; no need for local logic anymore. --- osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index eec2cd6a60..9a5db4bb39 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -29,12 +29,7 @@ namespace osu.Game.Storyboards.Drawables [BackgroundDependencyLoader(true)] private void load(IBindable beatmap, TextureStore textureStore) { - string? path = beatmap.Value.BeatmapSetInfo?.GetPathForFile(Video.Path); - - if (path == null) - return; - - var stream = textureStore.GetStream(path); + var stream = textureStore.GetStream(Video.Path); if (stream == null) return; From 333b839e0d7b859aa677a6a27163cb894c7d5563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Sep 2023 21:37:44 +0200 Subject: [PATCH 1487/2100] Fix broken automatic beatmap download setting migration --- osu.Game/Configuration/OsuConfigManager.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index b5253d3500..db71ff4e84 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -64,6 +64,12 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.Username, string.Empty); SetDefault(OsuSetting.Token, string.Empty); +#pragma warning disable CS0618 // Type or member is obsolete + // this default set MUST remain despite the setting being deprecated, because `SetDefault()` calls are implicitly used to declare the type returned for the lookup. + // if this is removed, the setting will be interpreted as a string, and `Migrate()` will fail due to cast failure. + // can be removed 20240618 + SetDefault(OsuSetting.AutomaticallyDownloadWhenSpectating, false); +#pragma warning restore CS0618 // Type or member is obsolete SetDefault(OsuSetting.AutomaticallyDownloadMissingBeatmaps, false); SetDefault(OsuSetting.SavePassword, false).ValueChanged += enabled => @@ -218,7 +224,7 @@ namespace osu.Game.Configuration if (combined < 20230918) { #pragma warning disable CS0618 // Type or member is obsolete - SetValue(OsuSetting.AutomaticallyDownloadMissingBeatmaps, Get(OsuSetting.AutomaticallyDownloadWhenSpectating)); // can be removed 20240618 + SetValue(OsuSetting.AutomaticallyDownloadMissingBeatmaps, Get(OsuSetting.AutomaticallyDownloadWhenSpectating)); // can be removed 20240618 #pragma warning restore CS0618 // Type or member is obsolete } } From 8e16b1d50784e62123d4ab308985b186e3c4ecb3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Sep 2023 12:48:15 +0900 Subject: [PATCH 1488/2100] Simplify some maximum size specs --- .../Skinning/Legacy/LegacyFruitPiece.cs | 13 +++++++++---- .../HitCircles/Components/HitCircleOverlapMarker.cs | 2 +- .../HitCircles/Components/HitCirclePiece.cs | 2 +- .../Objects/Drawables/DrawableHitCircle.cs | 2 +- .../Objects/Drawables/DrawableSliderBall.cs | 2 +- .../Objects/Drawables/DrawableSliderRepeat.cs | 2 +- .../Objects/Drawables/DrawableSliderTail.cs | 2 +- .../Objects/Drawables/DrawableSliderTick.cs | 2 +- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 5 +++++ .../Skinning/Argon/ArgonMainCirclePiece.cs | 2 +- .../Skinning/Argon/ArgonReverseArrow.cs | 2 +- .../Skinning/Default/CirclePiece.cs | 3 +-- .../Skinning/Default/ExplodePiece.cs | 3 +-- .../Skinning/Default/FlashPiece.cs | 3 +-- .../Skinning/Default/MainCirclePiece.cs | 3 +-- .../Skinning/Default/ReverseArrowPiece.cs | 2 +- osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs | 3 +-- .../Skinning/Legacy/LegacyApproachCircle.cs | 2 +- .../Skinning/Legacy/LegacyMainCirclePiece.cs | 9 +++------ .../Skinning/Legacy/LegacyReverseArrow.cs | 3 +-- .../Skinning/Legacy/LegacySliderBall.cs | 5 ++--- .../Skinning/Legacy/OsuLegacySkinTransformer.cs | 4 ++-- .../UI/Cursor/CursorRippleVisualiser.cs | 2 +- 23 files changed, 39 insertions(+), 39 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs index eacda1dc64..62097d79bd 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs @@ -26,21 +26,26 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy switch (visualRepresentation) { case FruitVisualRepresentation.Pear: - SetTexture(Skin.GetTexture("fruit-pear")?.WithMaximumSize(fruit_max_size), Skin.GetTexture("fruit-pear-overlay")?.WithMaximumSize(fruit_max_size)); + setTextures("pear"); break; case FruitVisualRepresentation.Grape: - SetTexture(Skin.GetTexture("fruit-grapes")?.WithMaximumSize(fruit_max_size), Skin.GetTexture("fruit-grapes-overlay")?.WithMaximumSize(fruit_max_size)); + setTextures("grapes"); break; case FruitVisualRepresentation.Pineapple: - SetTexture(Skin.GetTexture("fruit-apple")?.WithMaximumSize(fruit_max_size), Skin.GetTexture("fruit-apple-overlay")?.WithMaximumSize(fruit_max_size)); + setTextures("apple"); break; case FruitVisualRepresentation.Raspberry: - SetTexture(Skin.GetTexture("fruit-orange")?.WithMaximumSize(fruit_max_size), Skin.GetTexture("fruit-orange-overlay")?.WithMaximumSize(fruit_max_size)); + setTextures("orange"); break; } + + void setTextures(string fruitName) => SetTexture( + Skin.GetTexture($"fruit-{fruitName}")?.WithMaximumSize(fruit_max_size), + Skin.GetTexture($"fruit-{fruitName}-overlay")?.WithMaximumSize(fruit_max_size) + ); } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs index e5cc8595d1..3cba0610a1 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components { Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; InternalChild = content = new Container { diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs index 670e98ca50..c585f09b00 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components { Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; CornerRadius = Size.X / 2; CornerExponent = 2; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 3458069dd1..999979c491 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -242,7 +242,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public HitReceptor() { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs index d06fb5b4de..47214f1e53 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Children = new[] { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index fc4863f164..5721328057 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private void load() { Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; AddInternal(scaleContainer = new Container { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index d9501f7d58..9fbc97c484 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private void load() { Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; AddRangeInternal(new Drawable[] { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 6d0ae93e62..a947580d2f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables [BackgroundDependencyLoader] private void load() { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Origin = Anchor.Centre; AddInternal(scaleContainer = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderScorePoint), _ => new CircularContainer diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index fd5741698a..0bdbfaa760 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -21,6 +21,11 @@ namespace osu.Game.Rulesets.Osu.Objects /// public const float OBJECT_RADIUS = 64; + /// + /// The width and height any element participating in display of a hitcircle (or similarly sized object) should be. + /// + public static readonly Vector2 OBJECT_DIMENSIONS = new Vector2(OBJECT_RADIUS * 2); + /// /// Scoring distance with a speed-adjusted beat length of 1 second (ie. the speed slider balls move through their track). /// diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs index 3427031dc8..7508a689d2 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon private Bindable configHitLighting = null!; - private static readonly Vector2 circle_size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + private static readonly Vector2 circle_size = OsuHitObject.OBJECT_DIMENSIONS; [Resolved] private DrawableHitObject drawableObject { get; set; } = null!; diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs index f93e26b2ca..67fc1b2304 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon Anchor = Anchor.Centre; Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; InternalChildren = new Drawable[] { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs index f4761e0ea8..65a7b1328b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; -using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Default { @@ -22,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default public CirclePiece() { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Masking = true; CornerRadius = Size.X / 2; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs index 91bf75617a..7beb16f7d7 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; -using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Default { @@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default public ExplodePiece() { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs index 789137117e..86087ac50d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs @@ -5,7 +5,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Osu.Objects; -using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Default { @@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { public FlashPiece() { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs index 20fa4e5342..bcea33f63c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Default @@ -25,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default public MainCirclePiece() { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/ReverseArrowPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/ReverseArrowPiece.cs index 3fe7872ff7..27868db2f6 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/ReverseArrowPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/ReverseArrowPiece.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default Anchor = Anchor.Centre; Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Child = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.ReverseArrow), _ => new SpriteIcon { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs index 46d48f62e7..c3bbd89ab6 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs @@ -5,7 +5,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Osu.Objects; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Default @@ -14,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { public RingPiece(float thickness = 9) { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs index cdc61ebd9b..403a14214e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private DrawableHitObject drawableObject { get; set; } = null!; public LegacyApproachCircle() - : base("Gameplay/osu/approachcircle", new Vector2(OsuHitObject.OBJECT_RADIUS * 2)) + : base("Gameplay/osu/approachcircle", OsuHitObject.OBJECT_DIMENSIONS) { } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index 18010cdb2c..8990204931 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -14,15 +14,12 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public partial class LegacyMainCirclePiece : CompositeDrawable { - private static readonly Vector2 circle_piece_size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); - public override bool RemoveCompletedTransforms => false; /// @@ -53,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy this.priorityLookupPrefix = priorityLookupPrefix; this.hasNumber = hasNumber; - Size = circle_piece_size; + Size = OsuHitObject.OBJECT_DIMENSIONS; } [BackgroundDependencyLoader] @@ -70,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png. InternalChildren = new[] { - CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(circle_piece_size) }) + CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS) }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -79,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d, maxSize: circle_piece_size)) + Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d, maxSize: OsuHitObject.OBJECT_DIMENSIONS)) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs index 293df6b3a0..3a80607522 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs @@ -10,7 +10,6 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Legacy @@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null); - InternalChild = arrow = (skin?.GetAnimation(lookupName, true, true, maxSize: new Vector2(OsuHitObject.OBJECT_RADIUS * 2)) ?? Empty()); + InternalChild = arrow = (skin?.GetAnimation(lookupName, true, true, maxSize: OsuHitObject.OBJECT_DIMENSIONS) ?? Empty()); textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin; } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs index 145a8a50af..c3beb5bc35 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs @@ -10,7 +10,6 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Legacy @@ -48,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = skin.GetTexture("sliderb-nd")?.WithMaximumSize(new Vector2(OsuHitObject.OBJECT_RADIUS * 2)), + Texture = skin.GetTexture("sliderb-nd")?.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS), Colour = new Color4(5, 5, 5, 255), }, LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d => @@ -60,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = skin.GetTexture("sliderb-spec")?.WithMaximumSize(new Vector2(OsuHitObject.OBJECT_RADIUS * 2)), + Texture = skin.GetTexture("sliderb-spec")?.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS), Blending = BlendingParameters.Additive, }, }; diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 2564dbf335..ea6f6fe6ce 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; case OsuSkinComponents.SliderBall: - var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "", maxSize: new Vector2(OsuHitObject.OBJECT_RADIUS * 2)); + var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "", maxSize: OsuHitObject.OBJECT_DIMENSIONS); // todo: slider ball has a custom frame delay based on velocity // Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME); @@ -139,7 +139,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy if (!this.HasFont(LegacyFont.HitCircle)) return null; - return new LegacySpriteText(LegacyFont.HitCircle, new Vector2(OsuHitObject.OBJECT_RADIUS * 2)) + return new LegacySpriteText(LegacyFont.HitCircle, OsuHitObject.OBJECT_DIMENSIONS) { // stable applies a blanket 0.8x scale to hitcircle fonts Scale = new Vector2(0.8f), diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs index 076d97d06a..52486b701a 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs @@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { new RingPiece(3) { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2), + Size = OsuHitObject.OBJECT_DIMENSIONS, Alpha = 0.1f, } }; From 50adb5f7a7991dcfd84eba98bb28155b65e32175 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Sep 2023 12:54:28 +0900 Subject: [PATCH 1489/2100] Remove incorrectly merge conflict resolved --- osu.Game/Storyboards/Drawables/DrawableStoryboard.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index a11251ed22..352246c533 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -23,8 +23,6 @@ namespace osu.Game.Storyboards.Drawables { public partial class DrawableStoryboard : Container { - public Vector2 AppliedScale { get; private set; } - [Cached(typeof(Storyboard))] public Storyboard Storyboard { get; } From b5e64d933c9d09196c0553d548e5dde0bbec8ed2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Sep 2023 12:54:36 +0900 Subject: [PATCH 1490/2100] Apply same fix to `DrawableStoryboardAnimation` --- .../Drawables/DrawableStoryboardAnimation.cs | 43 ++++++++++++------- .../Drawables/DrawableStoryboardSprite.cs | 6 +-- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 054a50456b..33f7a3c6f2 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -94,25 +94,19 @@ namespace osu.Game.Storyboards.Drawables [Resolved] private IBeatSyncProvider beatSyncProvider { get; set; } + [Resolved] + private TextureStore textureStore { get; set; } + [BackgroundDependencyLoader] - private void load(TextureStore textureStore, Storyboard storyboard) + private void load(Storyboard storyboard) { - int frameIndex = 0; - - Texture frameTexture = textureStore.Get(getFramePath(frameIndex)); - - if (frameTexture != null) + if (storyboard.UseSkinSprites) { - // sourcing from storyboard. - for (frameIndex = 0; frameIndex < Animation.FrameCount; frameIndex++) - AddFrame(textureStore.Get(getFramePath(frameIndex)), Animation.FrameDelay); - } - else if (storyboard.UseSkinSprites) - { - // fallback to skin if required. skin.SourceChanged += skinSourceChanged; skinSourceChanged(); } + else + addFramesFromStoryboardSource(); Animation.ApplyTransforms(this); } @@ -135,11 +129,28 @@ namespace osu.Game.Storyboards.Drawables // When reading from a skin, we match stables weird behaviour where `FrameCount` is ignored // and resources are retrieved until the end of the animation. - foreach (var texture in skin.GetTextures(Path.GetFileNameWithoutExtension(Animation.Path)!, default, default, true, string.Empty, out _)) - AddFrame(texture, Animation.FrameDelay); + var skinTextures = skin.GetTextures(Path.GetFileNameWithoutExtension(Animation.Path)!, default, default, true, string.Empty, out _); + + if (skinTextures.Length > 0) + { + foreach (var texture in skinTextures) + AddFrame(texture, Animation.FrameDelay); + } + else + { + addFramesFromStoryboardSource(); + } } - private string getFramePath(int i) => Animation.Path.Replace(".", $"{i}."); + private void addFramesFromStoryboardSource() + { + int frameIndex; + // sourcing from storyboard. + for (frameIndex = 0; frameIndex < Animation.FrameCount; frameIndex++) + AddFrame(textureStore.Get(getFramePath(frameIndex)), Animation.FrameDelay); + + string getFramePath(int i) => Animation.Path.Replace(".", $"{i}."); + } protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 14132654d1..ad344b6bd4 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.ComponentModel; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -78,9 +77,6 @@ namespace osu.Game.Storyboards.Drawables [Resolved] private ISkinSource skin { get; set; } = null!; - [Resolved] - private Storyboard storyboard { get; set; } = null!; - [Resolved] private TextureStore textureStore { get; set; } = null!; @@ -95,7 +91,7 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader] - private void load() + private void load(Storyboard storyboard) { if (storyboard.UseSkinSprites) { From bd66285bd47859569618a26eebbf2563d65d9f93 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Sep 2023 12:59:40 +0900 Subject: [PATCH 1491/2100] Rename parameter on `LegacySpriteText` to better imply the maximum size is per glyph --- osu.Game/Skinning/LegacySpriteText.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index f021a99102..7eb92126fa 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -13,7 +13,7 @@ namespace osu.Game.Skinning public sealed partial class LegacySpriteText : OsuSpriteText { private readonly LegacyFont font; - private readonly Vector2? maxSize; + private readonly Vector2? maxSizePerGlyph; private LegacyGlyphStore glyphStore = null!; @@ -21,10 +21,10 @@ namespace osu.Game.Skinning protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x' }; - public LegacySpriteText(LegacyFont font, Vector2? maxSize = null) + public LegacySpriteText(LegacyFont font, Vector2? maxSizePerGlyph = null) { this.font = font; - this.maxSize = maxSize; + this.maxSizePerGlyph = maxSizePerGlyph; Shadow = false; UseFullGlyphHeight = false; @@ -36,7 +36,7 @@ namespace osu.Game.Skinning Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: true); Spacing = new Vector2(-skin.GetFontOverlap(font), 0); - glyphStore = new LegacyGlyphStore(skin, maxSize); + glyphStore = new LegacyGlyphStore(skin, maxSizePerGlyph); } protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore); From 1316403180f1f4f095e71e205221eead5a481912 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Sep 2023 13:02:40 +0900 Subject: [PATCH 1492/2100] Fix inspection in new test scene --- .../Visual/Gameplay/TestSceneGameplayElementDimensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayElementDimensions.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayElementDimensions.cs index 20e6e5658c..ff7cf2a124 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayElementDimensions.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayElementDimensions.cs @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Gameplay return texture; } - public ISkin? FindProvider(Func lookupFunction) => this; + public ISkin FindProvider(Func lookupFunction) => this; public IEnumerable AllSources => new[] { this }; } } From 71ac5cfc792a2aec35720e08f6d02d87e69995be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Sep 2023 14:14:37 +0900 Subject: [PATCH 1493/2100] Don't bother binding to friends changes for score display purposes --- osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 502303e80c..7471955493 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -13,7 +13,6 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Scoring; using osu.Game.Users; using osu.Game.Users.Drawables; @@ -110,7 +109,7 @@ namespace osu.Game.Screens.Play.HUD private IBindable scoreDisplayMode = null!; - private readonly IBindableList apiFriends = new BindableList(); + private bool isFriend; /// /// Creates a new . @@ -317,8 +316,7 @@ namespace osu.Game.Screens.Play.HUD HasQuit.BindValueChanged(_ => updateState()); - apiFriends.BindTo(api.Friends); - apiFriends.BindCollectionChanged((_, _) => updateState()); + isFriend = User != null && api.Friends.Any(u => User.OnlineID == u.Id); } protected override void LoadComplete() @@ -397,7 +395,7 @@ namespace osu.Game.Screens.Play.HUD panelColour = BackgroundColour ?? Color4Extensions.FromHex("ffd966"); textColour = TextColour ?? Color4Extensions.FromHex("2e576b"); } - else if (apiFriends.Any(f => User?.Equals(f) == true)) + else if (isFriend) { panelColour = BackgroundColour ?? Color4Extensions.FromHex("ff549a"); textColour = TextColour ?? Color4.White; From c6cc858967ccc3068aa0b19a7e308451b6e16d97 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Sep 2023 15:27:30 +0900 Subject: [PATCH 1494/2100] Change implementation of "show speed changes" to require explicit ruleset support --- .../Edit/ScrollingHitObjectComposer.cs | 37 +++++++------------ .../ISupportConstantAlgorithmToggle.cs | 15 ++++++++ 2 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 osu.Game/Rulesets/UI/Scrolling/ISupportConstantAlgorithmToggle.cs diff --git a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs index 0340354016..75305a0c20 100644 --- a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI.Scrolling; @@ -28,34 +27,24 @@ namespace osu.Game.Rulesets.Edit [BackgroundDependencyLoader] private void load() { - if (DrawableRuleset is DrawableScrollingRuleset drawableScrollingRuleset) + if (DrawableRuleset is ISupportConstantAlgorithmToggle toggleRuleset) { - var originalVisualisationMethod = drawableScrollingRuleset.VisualisationMethod; - - if (originalVisualisationMethod != ScrollVisualisationMethod.Constant) + LeftToolbox.Add(new EditorToolboxGroup("playfield") { - LeftToolbox.Add(new EditorToolboxGroup("playfield") + Child = new FillFlowContainer { - Child = new FillFlowContainer + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5), - Children = new[] - { - new DrawableTernaryButton(new TernaryButton(showSpeedChanges, "Show speed changes", () => new SpriteIcon { Icon = FontAwesome.Solid.TachometerAlt })) - } - }, - }); + new DrawableTernaryButton(new TernaryButton(showSpeedChanges, "Show speed changes", () => new SpriteIcon { Icon = FontAwesome.Solid.TachometerAlt })) + } + }, + }); - showSpeedChanges.BindValueChanged(state => - { - drawableScrollingRuleset.VisualisationMethod = state.NewValue == TernaryState.True - ? originalVisualisationMethod - : ScrollVisualisationMethod.Constant; - }, true); - } + showSpeedChanges.BindValueChanged(state => toggleRuleset.ShowSpeedChanges.Value = state.NewValue == TernaryState.True, true); } } } diff --git a/osu.Game/Rulesets/UI/Scrolling/ISupportConstantAlgorithmToggle.cs b/osu.Game/Rulesets/UI/Scrolling/ISupportConstantAlgorithmToggle.cs new file mode 100644 index 0000000000..aaa635350e --- /dev/null +++ b/osu.Game/Rulesets/UI/Scrolling/ISupportConstantAlgorithmToggle.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; + +namespace osu.Game.Rulesets.UI.Scrolling +{ + /// + /// Denotes a which supports toggling constant algorithm for better display in the editor. + /// + public interface ISupportConstantAlgorithmToggle : IDrawableScrollingRuleset + { + public BindableBool ShowSpeedChanges { get; } + } +} From 41a8239e49d87fec622135874bddcf2660ae0000 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Sep 2023 15:27:50 +0900 Subject: [PATCH 1495/2100] Remvoe null default for mods which can't be null --- osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs | 2 +- osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 2 +- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 136a78b343..d74e6194fb 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Catch.Edit return base.OnPressed(e); } - protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods = null) => + protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) => new DrawableCatchEditorRuleset(ruleset, beatmap, mods) { TimeRangeMultiplier = { BindTarget = timeRangeMultiplier, } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 9bde9485b2..8e61baca81 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mania.Edit protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => Playfield.GetColumnByPosition(screenSpacePosition); - protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) { drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index cff2171cbd..fdc11be42c 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Edit { } - protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) => new DrawableOsuEditorRuleset(ruleset, beatmap, mods); protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 295a016c7b..f9a6b5083e 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -307,7 +307,7 @@ namespace osu.Game.Rulesets.Edit /// The loaded beatmap. /// The mods to be applied. /// An editor-relevant . - protected virtual DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + protected virtual DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) => (DrawableRuleset)ruleset.CreateDrawableRulesetWith(beatmap, mods); #region Tool selection logic From cb0226f84356ae0fe991cae3664590b3c6dcc708 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Sep 2023 15:28:13 +0900 Subject: [PATCH 1496/2100] Implement new interface-based speed change visualisation support on mania/taiko --- .../Edit/DrawableManiaEditorRuleset.cs | 13 ++++++- .../Edit/DrawableTaikoEditorRuleset.cs | 37 +++++++++++++++++++ .../Edit/TaikoHitObjectComposer.cs | 6 +++ .../UI/DrawableTaikoRuleset.cs | 7 +++- 4 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Edit/DrawableTaikoEditorRuleset.cs diff --git a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs index 1741dad5d6..7b019a2bdf 100644 --- a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs +++ b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -12,8 +14,10 @@ using osuTK; namespace osu.Game.Rulesets.Mania.Edit { - public partial class DrawableManiaEditorRuleset : DrawableManiaRuleset + public partial class DrawableManiaEditorRuleset : DrawableManiaRuleset, ISupportConstantAlgorithmToggle { + public BindableBool ShowSpeedChanges { get; set; } = new BindableBool(); + public new IScrollingInfo ScrollingInfo => base.ScrollingInfo; public DrawableManiaEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods) @@ -21,6 +25,13 @@ namespace osu.Game.Rulesets.Mania.Edit { } + protected override void LoadComplete() + { + base.LoadComplete(); + + ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Sequential : ScrollVisualisationMethod.Constant, true); + } + protected override Playfield CreatePlayfield() => new ManiaEditorPlayfield(Beatmap.Stages) { Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko/Edit/DrawableTaikoEditorRuleset.cs b/osu.Game.Rulesets.Taiko/Edit/DrawableTaikoEditorRuleset.cs new file mode 100644 index 0000000000..963ddec0b3 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/DrawableTaikoEditorRuleset.cs @@ -0,0 +1,37 @@ +// 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 osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI.Scrolling; + +namespace osu.Game.Rulesets.Taiko.Edit +{ + public partial class DrawableTaikoEditorRuleset : DrawableTaikoRuleset, ISupportConstantAlgorithmToggle + { + public BindableBool ShowSpeedChanges { get; set; } = new BindableBool(); + + public DrawableTaikoEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) + : base(ruleset, beatmap, mods) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Overlapping : ScrollVisualisationMethod.Constant, true); + } + + protected override double ComputeTimeRange() + { + // Adjust when we're using constant algorithm to not be sluggish. + double multiplier = ShowSpeedChanges.Value ? 1 : 4; + return base.ComputeTimeRange() / multiplier; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs index fbad8c7fad..5ae4757b8f 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs @@ -2,9 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Compose.Components; namespace osu.Game.Rulesets.Taiko.Edit @@ -25,6 +28,9 @@ namespace osu.Game.Rulesets.Taiko.Edit new SwellCompositionTool() }; + protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) => + new DrawableTaikoEditorRuleset(ruleset, beatmap, mods); + protected override ComposeBlueprintContainer CreateBlueprintContainer() => new TaikoBlueprintContainer(this); } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index 979e03f201..2af4c0c2e8 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -64,6 +64,11 @@ namespace osu.Game.Rulesets.Taiko.UI { base.Update(); + TimeRange.Value = ComputeTimeRange(); + } + + protected virtual double ComputeTimeRange() + { // Taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened. const float scroll_rate = 10; @@ -72,7 +77,7 @@ namespace osu.Game.Rulesets.Taiko.UI // We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default. float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT); - TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate; + return (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate; } protected override void UpdateAfterChildren() From d7129da8ea708f5485e023a18012940b1d5d3dfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 20 Sep 2023 12:05:23 +0200 Subject: [PATCH 1497/2100] Fix `TestSceneDrawableStoryboardSprite` not displaying anything --- .../Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs index d20c7c2f7a..32693c2bb2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs @@ -180,7 +180,7 @@ namespace osu.Game.Tests.Visual.Gameplay layer.Elements.Clear(); layer.Add(sprite); - return storyboard.CreateDrawable(); + return storyboard.CreateDrawable().With(s => s.RelativeSizeAxes = Axes.Both); } private void assertStoryboardSourced() From f2791d4f3e1c3067d8a2b9fcbab013edcd5eeefb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 20 Sep 2023 12:22:05 +0200 Subject: [PATCH 1498/2100] Move comment a bit to fix formatting Would otherwise trigger IDE0055, but that isn't resolveable without an inspection cycle with resharper, so just move in a more sane place. --- osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index d74e6194fb..dc3a4416a5 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -25,8 +25,8 @@ using osuTK; namespace osu.Game.Rulesets.Catch.Edit { + // we're also a ScrollingHitObjectComposer candidate, but can't be everything can we? public partial class CatchHitObjectComposer : DistancedHitObjectComposer - // we're also a ScrollingHitObjectComposer candidate, but can't be everything can we? { private const float distance_snap_radius = 50; From bf984388b364ba4a3a35139df08b09fa0aac93c5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Sep 2023 19:12:55 +0900 Subject: [PATCH 1499/2100] Update clocks in line with framework changes --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 2 -- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 2 -- osu.Game/Screens/Edit/EditorClock.cs | 2 -- osu.Game/Screens/Play/GameplayClockContainer.cs | 2 -- 4 files changed, 8 deletions(-) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 9577d1e38b..62484fa12b 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -216,8 +216,6 @@ namespace osu.Game.Beatmaps public double FramesPerSecond => finalClockSource.FramesPerSecond; - public FrameTimeInfo TimeInfo => finalClockSource.TimeInfo; - #endregion protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 90cffab714..2af9916a6b 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -264,8 +264,6 @@ namespace osu.Game.Rulesets.UI public double FramesPerSecond => framedClock.FramesPerSecond; - public FrameTimeInfo TimeInfo => framedClock.TimeInfo; - public double StartTime => parentGameplayClock?.StartTime ?? 0; private readonly AudioAdjustments gameplayAdjustments = new AudioAdjustments(); diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index e5e88a04d9..a05a873101 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -231,8 +231,6 @@ namespace osu.Game.Screens.Edit public double FramesPerSecond => underlyingClock.FramesPerSecond; - public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo; - public void ChangeSource(IClock source) { track.Value = source as Track; diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 20bf6c3829..2478af1dd4 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -234,7 +234,5 @@ namespace osu.Game.Screens.Play public double ElapsedFrameTime => GameplayClock.ElapsedFrameTime; public double FramesPerSecond => GameplayClock.FramesPerSecond; - - public FrameTimeInfo TimeInfo => GameplayClock.TimeInfo; } } From 8a3d412ffc3374c4ad99cd4b26b00edf77e4cf35 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Sep 2023 11:38:16 +0900 Subject: [PATCH 1500/2100] Remove mention of no-heated-gameplay-mechanics discussions We're kinda at the point we're allowing this now. --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 792e2d646a..ce9fe4d053 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,6 @@ The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Curre This project is under constant development, but we aim to keep things in a stable state. Users are encouraged to try it out and keep it installed alongside the stable *osu!* client. It will continue to evolve to the point of eventually replacing the existing stable client as an update. -**IMPORTANT:** Gameplay mechanics (and other features which you may have come to know and love) are in a constant state of flux. Game balance and final quality-of-life passes come at the end of development, preceded by experimentation and changes which may potentially **reduce playability or usability**. This is done in order to allow us to move forward as developers and designers more efficiently. If this offends you, please consider sticking to a [stable release](https://osu.ppy.sh/home/download) of osu!. We are not yet open to heated discussion over game mechanics and will not be using github as a forum for such discussions just yet. - We are accepting bug reports (please report with as much detail as possible and follow the existing issue templates). Feature requests are also welcome, but understand that our focus is on completing the game to feature parity before adding new features. A few resources are available as starting points to getting involved and understanding the project: - Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer). From 2954ad78349e126482c02687f98fcf00055314ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Sep 2023 11:56:23 +0900 Subject: [PATCH 1501/2100] Update language across whole readme to read better --- README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ce9fe4d053..a9dac3d6cf 100644 --- a/README.md +++ b/README.md @@ -12,33 +12,35 @@ A free-to-win rhythm game. Rhythm is just a *click* away! -The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge. +This is the future – and final – iteration of the [osu!](https://osu.ppy.sh) game client and marks the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge. ## Status -This project is under constant development, but we aim to keep things in a stable state. Users are encouraged to try it out and keep it installed alongside the stable *osu!* client. It will continue to evolve to the point of eventually replacing the existing stable client as an update. +This project is under constant development, but we do our best to keep things in a stable state. Players are encouraged to install from a release alongside their stable *osu!* client. This project will continue to evolve until we eventually reach the point where most users prefer it over the previous "osu!stable" release. -We are accepting bug reports (please report with as much detail as possible and follow the existing issue templates). Feature requests are also welcome, but understand that our focus is on completing the game to feature parity before adding new features. A few resources are available as starting points to getting involved and understanding the project: +A few resources are available as starting points to getting involved and understanding the project: - Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer). - You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management). ## Running osu! -If you are looking to install or test osu! without setting up a development environment, you can consume our [releases](https://github.com/ppy/osu/releases). You can also generally download a version for your current device from the [osu! site](https://osu.ppy.sh/home/download). Failing that, you may use the links below to download the latest version for your operating system of choice: +If you are just looking to give the game a whirl, you can grab the latest release for your platform: -**Latest release:** +### Latest release: | [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 10.15+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 13.4+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) | | ------------- | ------------- | ------------- | ------------- | ------------- | -- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets. +You can also generally download a version for your current device from the [osu! site](https://osu.ppy.sh/home/download) If your platform is not listed above, there is still a chance you can manually build it by following the instructions below. +**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores in early 2024. + ## Developing a custom ruleset -osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates). +osu! is designed to allow user-created gameplay variations, called "rulesets". Building one of these allows a developer to harness the power of the osu! beatmap library, game engine, and general UX for a new style of gameplay. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates). You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/discussions/13096). From c76853c32c6267110657ef7c31c81f58e47d7191 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Sep 2023 11:56:32 +0900 Subject: [PATCH 1502/2100] Add mention of new project --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a9dac3d6cf..2cf3b4bf6b 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ A few resources are available as starting points to getting involved and underst - Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer). - You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management). +- Track our current efforts [towards full "ranked play" support](https://github.com/orgs/ppy/projects/13?query=is%3Aopen+sort%3Aupdated-desc) ## Running osu! From fc6abae968011aecada1d7f81484b78f2a2b0d64 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Sep 2023 11:56:49 +0900 Subject: [PATCH 1503/2100] Remove note about `dotnet` CLI tools not working (less relevant post-EF) --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 2cf3b4bf6b..3966a9258a 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,6 @@ If you are not interested in debugging *osu!*, you can add `-c Release` to gain If the build fails, try to restore NuGet packages with `dotnet restore`. -_Due to a historical feature gap between .NET Core and Xamarin, running `dotnet` CLI from the root directory will not work for most commands. This can be resolved by specifying a target `.csproj` or the helper project at `build/Desktop.proj`. Configurations have been provided to work around this issue for all supported IDEs mentioned above._ - ### Testing with resource/framework modifications Sometimes it may be necessary to cross-test changes in [osu-resources](https://github.com/ppy/osu-resources) or [osu-framework](https://github.com/ppy/osu-framework). This can be quickly achieved using included commands: From 9629f49afbd26dcf65674b6cf0a2672e67018920 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Sep 2023 11:57:10 +0900 Subject: [PATCH 1504/2100] Update build instructions to be more clear about `slnf` files and mention `workload`s --- README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3966a9258a..6009ff4d59 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ You can see some examples of custom rulesets by visiting the [custom ruleset dir ## Developing osu! +### Prerequisites + Please make sure you have the following prerequisites: - A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download) installed. @@ -70,9 +72,19 @@ git pull ### Building -Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing). +#### From an IDE -- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln`. This will allow access to template run configurations. +You should load the solution via one of the platform-specific `.slnf` files, rather than the main `.sln`. This will reduce dependencies and hide platforms that you don't care about. Valid `.slnf` files are: + +- `osu.Desktop.slnf` (most common) +- `osu.Android.slnf` +- `osu.iOS.slnf` + +Run configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing). + +To build for mobile platforms, you will likely need to run `sudo dotnet workload restore` if you haven't done so previously. This will install android/iOS tooling required to complete the build. + +#### From CLI You can also build and run *osu!* from the command-line with a single command: From 262916787ecfde13d2499aa733611a845fa13e13 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Sep 2023 13:27:00 +0900 Subject: [PATCH 1505/2100] Apply punctuation and terminology fixes Co-authored-by: Joseph Madamba --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6009ff4d59..946a6b03d9 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ A few resources are available as starting points to getting involved and underst - Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer). - You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management). -- Track our current efforts [towards full "ranked play" support](https://github.com/orgs/ppy/projects/13?query=is%3Aopen+sort%3Aupdated-desc) +- Track our current efforts [towards full "ranked play" support](https://github.com/orgs/ppy/projects/13?query=is%3Aopen+sort%3Aupdated-desc). ## Running osu! @@ -33,7 +33,7 @@ If you are just looking to give the game a whirl, you can grab the latest releas | [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 10.15+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 13.4+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) | | ------------- | ------------- | ------------- | ------------- | ------------- | -You can also generally download a version for your current device from the [osu! site](https://osu.ppy.sh/home/download) +You can also generally download a version for your current device from the [osu! site](https://osu.ppy.sh/home/download). If your platform is not listed above, there is still a chance you can manually build it by following the instructions below. @@ -80,9 +80,9 @@ You should load the solution via one of the platform-specific `.slnf` files, rat - `osu.Android.slnf` - `osu.iOS.slnf` -Run configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing). +Run configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `osu! (Tests)` project/configuration. More information on this is provided [below](#contributing). -To build for mobile platforms, you will likely need to run `sudo dotnet workload restore` if you haven't done so previously. This will install android/iOS tooling required to complete the build. +To build for mobile platforms, you will likely need to run `sudo dotnet workload restore` if you haven't done so previously. This will install Android/iOS tooling required to complete the build. #### From CLI From 0eab4c5364d23e3bb996894ae3fd58b4842f0914 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Sep 2023 14:47:55 +0900 Subject: [PATCH 1506/2100] Reword sentence with multiple `and`s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 946a6b03d9..f7a4936e50 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ A free-to-win rhythm game. Rhythm is just a *click* away! -This is the future – and final – iteration of the [osu!](https://osu.ppy.sh) game client and marks the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge. +This is the future – and final – iteration of the [osu!](https://osu.ppy.sh) game client which marks the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge. ## Status From 8ef0ef09db1381df7fc0bf0f65701d4ca2ca8a1c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Sep 2023 14:59:37 +0900 Subject: [PATCH 1507/2100] Reword release build disclaimer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f7a4936e50..d5dc0723af 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ You can also build and run *osu!* from the command-line with a single command: dotnet run --project osu.Desktop ``` -If you are not interested in debugging *osu!*, you can add `-c Release` to gain performance. In this case, you must replace `Debug` with `Release` in any commands mentioned in this document. +When running locally to do any kind of performance testing, make sure to add `-c Release` to the build command, as the overhead of running with the default `Debug` configuration can be large (especially when testing with local framework modifications as below). If the build fails, try to restore NuGet packages with `dotnet restore`. From c4fc4199d190b6bff29d10c65341b9c7a011fca9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 21 Sep 2023 19:02:31 +0300 Subject: [PATCH 1508/2100] Use correct maximum size for droplets --- osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs index 581259a9c4..c6c0839fba 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy { public partial class LegacyDropletPiece : LegacyCatchHitObjectPiece { - private static readonly Vector2 droplet_max_size = new Vector2(100); + private static readonly Vector2 droplet_max_size = new Vector2(82, 103); public LegacyDropletPiece() { From ad86bf2d56cb85e5bd4b84e74a92d1ed63aaf578 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 21 Sep 2023 19:03:08 +0300 Subject: [PATCH 1509/2100] Revert redundant size limitations Already handled by the sprites themselves being resized. --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs index 83f05fe6ec..5543a31ec9 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Graphics; using osu.Game.Skinning; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Legacy @@ -48,13 +47,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Anchor = Anchor.CentreRight, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, - Texture = skin.GetTexture("taiko-roll-end", WrapMode.ClampToEdge, WrapMode.ClampToEdge)?.WithMaximumSize(new Vector2(128, 256)), + Texture = skin.GetTexture("taiko-roll-end", WrapMode.ClampToEdge, WrapMode.ClampToEdge), FillMode = FillMode.Fit, }, body = new Sprite { RelativeSizeAxes = Axes.Both, - Texture = skin.GetTexture("taiko-roll-middle", WrapMode.ClampToEdge, WrapMode.ClampToEdge)?.WithMaximumSize(new Vector2(2, 256)), + Texture = skin.GetTexture("taiko-roll-middle", WrapMode.ClampToEdge, WrapMode.ClampToEdge), }, headCircle = new LegacyCirclePiece { From 9af4e75dfc5dd5cdfd5e70c227d1a48e2747205c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Sep 2023 01:24:24 +0900 Subject: [PATCH 1510/2100] Disable clipboard export for song select textbox In combination with https://github.com/ppy/osu-framework/pull/5997, closes https://github.com/ppy/osu/issues/24867 --- osu.Game/Screens/Select/FilterControl.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 38520a85b7..614c9bd7ec 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -254,6 +254,8 @@ namespace osu.Game.Screens.Select public OsuSpriteText FilterText { get; private set; } + protected override bool AllowClipboardExport => false; + public FilterControlTextBox() { Height += filter_text_size; From f1258a396367d4ccec91977c34f40ca13cb4dd6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Sep 2023 01:26:38 +0900 Subject: [PATCH 1511/2100] Update framework --- 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 10cee77b09..20b0f220a3 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + System.Net.Sockets.SocketException (11001): No such host is known. 2023-10-06 03:24:17 [verbose]: at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error, CancellationToken cancellationToken) ``` Closes https://github.com/ppy/osu/issues/24890 (again). --- osu.Game/Online/API/OAuth.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/OAuth.cs b/osu.Game/Online/API/OAuth.cs index 1f26ab5458..485274f349 100644 --- a/osu.Game/Online/API/OAuth.cs +++ b/osu.Game/Online/API/OAuth.cs @@ -6,6 +6,7 @@ using System; using System.Diagnostics; using System.Net.Http; +using System.Net.Sockets; using Newtonsoft.Json; using osu.Framework.Bindables; @@ -99,6 +100,11 @@ namespace osu.Game.Online.API return true; } } + catch (SocketException) + { + // Network failure. + return false; + } catch (HttpRequestException) { // Network failure. @@ -106,7 +112,7 @@ namespace osu.Game.Online.API } catch { - // Force a full re-reauthentication. + // Force a full re-authentication. Token.Value = null; return false; } From db5178e45306ce9df560b047a563b3e075a75ff6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Oct 2023 16:52:00 +0900 Subject: [PATCH 1700/2100] Change `ArgonHealthDisplay` to be relative sized for now --- .../Gameplay/TestSceneArgonHealthDisplay.cs | 2 - .../Screens/Play/HUD/ArgonHealthDisplay.cs | 68 ++++++++++++++----- 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs index 06a7763711..8261a1729e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs @@ -12,7 +12,6 @@ using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; -using osuTK; using osuTK.Graphics; namespace osu.Game.Tests.Visual.Gameplay @@ -41,7 +40,6 @@ namespace osu.Game.Tests.Visual.Gameplay { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Scale = new Vector2(2f), }, }; }); diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 62a4b958c2..ad4b407692 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -5,14 +5,17 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Shapes; +using osu.Framework.Layout; using osu.Framework.Threading; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -27,6 +30,23 @@ namespace osu.Game.Screens.Play.HUD { public bool UsesFixedAnchor { get; set; } + [SettingSource("Bar height")] + public BindableFloat BarHeight { get; } = new BindableFloat + { + Default = 32, + MinValue = 0, + MaxValue = 64, + Precision = 1 + }; + + [SettingSource("Bar length")] + public BindableFloat BarLength { get; } = new BindableFloat(1) + { + MinValue = 0.2f, + MaxValue = 1, + Precision = 0.01f, + }; + private BarPath mainBar = null!; /// @@ -76,10 +96,13 @@ namespace osu.Game.Screens.Play.HUD } } + private const float left_line_width = 50f; + [BackgroundDependencyLoader] private void load() { - AutoSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; InternalChild = new FillFlowContainer { @@ -91,7 +114,7 @@ namespace osu.Game.Screens.Play.HUD new Circle { Margin = new MarginPadding { Top = 8.5f, Left = -2 }, - Size = new Vector2(50f, 3f), + Size = new Vector2(left_line_width, 3f), }, new Container { @@ -127,8 +150,6 @@ namespace osu.Game.Screens.Play.HUD } }, }; - - updatePath(); } protected override void LoadComplete() @@ -144,6 +165,18 @@ namespace osu.Game.Screens.Play.HUD if (resetMissBarDelegate == null) this.TransformTo(nameof(GlowBarValue), v.NewValue, 300, Easing.OutQuint); }, true); + + BarLength.BindValueChanged(l => Width = l.NewValue, true); + BarHeight.BindValueChanged(_ => updatePath()); + updatePath(); + } + + protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) + { + if ((invalidation & Invalidation.DrawSize) > 0) + updatePath(); + + return base.OnInvalidate(invalidation, source); } protected override void Update() @@ -214,25 +247,24 @@ namespace osu.Game.Screens.Play.HUD private void updatePath() { - const float curve_start = 280; - const float curve_end = 310; + float barLength = DrawWidth - left_line_width - 24; + float curveStart = barLength - 70; + float curveEnd = barLength - 40; + const float curve_smoothness = 10; - const float bar_length = 350; - const float bar_verticality = 32.5f; - - Vector2 diagonalDir = (new Vector2(curve_end, bar_verticality) - new Vector2(curve_start, 0)).Normalized(); + Vector2 diagonalDir = (new Vector2(curveEnd, BarHeight.Value) - new Vector2(curveStart, 0)).Normalized(); barPath = new SliderPath(new[] { new PathControlPoint(new Vector2(0, 0), PathType.Linear), - new PathControlPoint(new Vector2(curve_start - curve_smoothness, 0), PathType.Bezier), - new PathControlPoint(new Vector2(curve_start, 0)), - new PathControlPoint(new Vector2(curve_start, 0) + diagonalDir * curve_smoothness, PathType.Linear), - new PathControlPoint(new Vector2(curve_end, bar_verticality) - diagonalDir * curve_smoothness, PathType.Bezier), - new PathControlPoint(new Vector2(curve_end, bar_verticality)), - new PathControlPoint(new Vector2(curve_end + curve_smoothness, bar_verticality), PathType.Linear), - new PathControlPoint(new Vector2(bar_length, bar_verticality)), + new PathControlPoint(new Vector2(curveStart - curve_smoothness, 0), PathType.Bezier), + new PathControlPoint(new Vector2(curveStart, 0)), + new PathControlPoint(new Vector2(curveStart, 0) + diagonalDir * curve_smoothness, PathType.Linear), + new PathControlPoint(new Vector2(curveEnd, BarHeight.Value) - diagonalDir * curve_smoothness, PathType.Bezier), + new PathControlPoint(new Vector2(curveEnd, BarHeight.Value)), + new PathControlPoint(new Vector2(curveEnd + curve_smoothness, BarHeight.Value), PathType.Linear), + new PathControlPoint(new Vector2(barLength, BarHeight.Value)), }); List vertices = new List(); @@ -267,7 +299,7 @@ namespace osu.Game.Screens.Play.HUD { protected override Color4 ColourAt(float position) { - if (position <= 0.128f) + if (position <= 0.16f) return Color4.White.Opacity(0.8f); return Interpolation.ValueAt(position, From f40e910c51da3f4ad5779b854941ffb5b8e53a23 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Oct 2023 18:56:31 +0900 Subject: [PATCH 1701/2100] Remove left line from health display --- .../Screens/Play/HUD/ArgonHealthDisplay.cs | 67 +++++++------------ 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index ad4b407692..67f21a1c83 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; -using osu.Framework.Graphics.Shapes; using osu.Framework.Layout; using osu.Framework.Threading; using osu.Framework.Utils; @@ -96,7 +95,7 @@ namespace osu.Game.Screens.Play.HUD } } - private const float left_line_width = 50f; + private const float main_path_radius = 10f; [BackgroundDependencyLoader] private void load() @@ -104,51 +103,37 @@ namespace osu.Game.Screens.Play.HUD RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - InternalChild = new FillFlowContainer + InternalChild = new Container { AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(4f, 0f), Children = new Drawable[] { - new Circle + background = new BackgroundPath { - Margin = new MarginPadding { Top = 8.5f, Left = -2 }, - Size = new Vector2(left_line_width, 3f), + PathRadius = main_path_radius, }, - new Container + glowBar = new BarPath { - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - background = new BackgroundPath - { - PathRadius = 10f, - }, - glowBar = new BarPath - { - BarColour = Color4.White, - GlowColour = OsuColour.Gray(0.5f), - Blending = BlendingParameters.Additive, - Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.8f), Color4.White), - PathRadius = 40f, - // Kinda hacky, but results in correct positioning with increased path radius. - Margin = new MarginPadding(-30f), - GlowPortion = 0.9f, - }, - mainBar = new BarPath - { - AutoSizeAxes = Axes.None, - RelativeSizeAxes = Axes.Both, - Blending = BlendingParameters.Additive, - BarColour = main_bar_colour, - GlowColour = main_bar_glow_colour, - PathRadius = 10f, - GlowPortion = 0.6f, - }, - } - } - }, + BarColour = Color4.White, + GlowColour = OsuColour.Gray(0.5f), + Blending = BlendingParameters.Additive, + Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.8f), Color4.White), + PathRadius = 40f, + // Kinda hacky, but results in correct positioning with increased path radius. + Margin = new MarginPadding(-30f), + GlowPortion = 0.9f, + }, + mainBar = new BarPath + { + AutoSizeAxes = Axes.None, + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + BarColour = main_bar_colour, + GlowColour = main_bar_glow_colour, + PathRadius = main_path_radius, + GlowPortion = 0.6f, + }, + } }; } @@ -247,7 +232,7 @@ namespace osu.Game.Screens.Play.HUD private void updatePath() { - float barLength = DrawWidth - left_line_width - 24; + float barLength = DrawWidth - main_path_radius * 2; float curveStart = barLength - 70; float curveEnd = barLength - 40; From 71be3c8f8b783716d4bcb619d36e253b1ad79315 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Oct 2023 18:56:16 +0900 Subject: [PATCH 1702/2100] Add ability to adjust health bar settings in test scene --- .../Gameplay/TestSceneArgonHealthDisplay.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs index 8261a1729e..7bad623d7f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; @@ -21,6 +22,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(HealthProcessor))] private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); + private ArgonHealthDisplay healthDisplay = null!; + [SetUpSteps] public void SetUpSteps() { @@ -36,13 +39,25 @@ namespace osu.Game.Tests.Visual.Gameplay RelativeSizeAxes = Axes.Both, Colour = Color4.Gray, }, - new ArgonHealthDisplay + healthDisplay = new ArgonHealthDisplay { Anchor = Anchor.Centre, Origin = Anchor.Centre, }, }; }); + + AddSliderStep("Width", 0, 1f, 1f, val => + { + if (healthDisplay.IsNotNull()) + healthDisplay.BarLength.Value = val; + }); + + AddSliderStep("Height", 0, 64, 0, val => + { + if (healthDisplay.IsNotNull()) + healthDisplay.BarHeight.Value = val; + }); } [Test] From 3f2a00d90d2a967147e33e273dab24779fb03747 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Oct 2023 18:46:50 +0900 Subject: [PATCH 1703/2100] Add argon health display to default skin layout --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 5 ++--- osu.Game/Skinning/ArgonSkin.cs | 10 +++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 67f21a1c83..755eaeaf33 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -30,16 +30,15 @@ namespace osu.Game.Screens.Play.HUD public bool UsesFixedAnchor { get; set; } [SettingSource("Bar height")] - public BindableFloat BarHeight { get; } = new BindableFloat + public BindableFloat BarHeight { get; } = new BindableFloat(20) { - Default = 32, MinValue = 0, MaxValue = 64, Precision = 1 }; [SettingSource("Bar length")] - public BindableFloat BarLength { get; } = new BindableFloat(1) + public BindableFloat BarLength { get; } = new BindableFloat(0.98f) { MinValue = 0.2f, MaxValue = 1, diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 6e17458082..d530efbfdd 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -109,6 +109,7 @@ namespace osu.Game.Skinning case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container => { + var health = container.OfType().FirstOrDefault(); var score = container.OfType().FirstOrDefault(); var accuracy = container.OfType().FirstOrDefault(); var combo = container.OfType().FirstOrDefault(); @@ -128,6 +129,13 @@ namespace osu.Game.Skinning score.Position = new Vector2(0, vertical_offset); + if (health != null) + { + health.Origin = Anchor.TopCentre; + health.Anchor = Anchor.TopCentre; + health.Y = 5; + } + if (ppCounter != null) { ppCounter.Y = score.Position.Y + ppCounter.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).Y - 4; @@ -191,7 +199,7 @@ namespace osu.Game.Skinning new DefaultComboCounter(), new DefaultScoreCounter(), new DefaultAccuracyCounter(), - new DefaultHealthDisplay(), + new ArgonHealthDisplay(), new ArgonSongProgress(), new ArgonKeyCounterDisplay(), new BarHitErrorMeter(), From d87ab9c82dad1081c6a060ea8b6e401bbd29cdee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Oct 2023 19:34:38 +0900 Subject: [PATCH 1704/2100] Adjust transition time based on miss/hit --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 755eaeaf33..7af7fd9487 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -145,9 +145,11 @@ namespace osu.Game.Screens.Play.HUD if (v.NewValue >= GlowBarValue) finishMissDisplay(); - this.TransformTo(nameof(HealthBarValue), v.NewValue, 300, Easing.OutQuint); + double time = v.NewValue > GlowBarValue ? 500 : 250; + + this.TransformTo(nameof(HealthBarValue), v.NewValue, time, Easing.OutQuint); if (resetMissBarDelegate == null) - this.TransformTo(nameof(GlowBarValue), v.NewValue, 300, Easing.OutQuint); + this.TransformTo(nameof(GlowBarValue), v.NewValue, time, Easing.OutQuint); }, true); BarLength.BindValueChanged(l => Width = l.NewValue, true); From 8e5b2e78e58842721478fbf5dd7fdd7c7a1f66ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Oct 2023 21:01:23 +0900 Subject: [PATCH 1705/2100] Fix variable clash --- osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 54cd36d05b..dd6536cf26 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -117,17 +117,17 @@ namespace osu.Game.Screens.OnlinePlay private void updateModDisplay() { - int current = Current.Value.Count; + int currentCount = Current.Value.Count; - if (current == allAvailableAndValidMods.Count()) + if (currentCount == allAvailableAndValidMods.Count()) { count.Text = "all"; count.FadeColour(colours.Gray2, 200, Easing.OutQuint); circle.FadeColour(colours.Yellow, 200, Easing.OutQuint); } - else if (current > 0) + else if (currentCount > 0) { - count.Text = $"{current} mods"; + count.Text = $"{currentCount} mods"; count.FadeColour(colours.Gray2, 200, Easing.OutQuint); circle.FadeColour(colours.YellowDark, 200, Easing.OutQuint); } From 10ce5705ce67acb920f2f4f0d6b30e91178df5d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Oct 2023 14:11:41 +0200 Subject: [PATCH 1706/2100] Update framework --- 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 bc95e96a7b..b3feccbbc0 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + + From 910d74fdda85433e2cf6b1f62daa3edc4a75f816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 11:59:38 +0200 Subject: [PATCH 2014/2100] Add failing test coverage for double-click on disabled sliders --- .../UserInterface/TestSceneRoundedSliderBar.cs | 17 +++++++++++++++++ .../UserInterface/TestSceneShearedSliderBar.cs | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs index 419e88137c..ae52c26fd2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs @@ -55,5 +55,22 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("slider is default", () => slider.Current.IsDefault); } + + [Test] + public void TestNubDoubleClickOnDisabledSliderDoesNothing() + { + AddStep("set slider to 1", () => slider.Current.Value = 1); + AddStep("disable slider", () => slider.Current.Disabled = true); + + AddStep("move mouse to nub", () => InputManager.MoveMouseTo(slider.ChildrenOfType().Single())); + + AddStep("double click nub", () => + { + InputManager.Click(MouseButton.Left); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("slider is still at 1", () => slider.Current.Value, () => Is.EqualTo(1)); + } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs index d459a2c701..334fc4563a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs @@ -55,5 +55,22 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("slider is default", () => slider.Current.IsDefault); } + + [Test] + public void TestNubDoubleClickOnDisabledSliderDoesNothing() + { + AddStep("set slider to 1", () => slider.Current.Value = 1); + AddStep("disable slider", () => slider.Current.Disabled = true); + + AddStep("move mouse to nub", () => InputManager.MoveMouseTo(slider.ChildrenOfType().Single())); + + AddStep("double click nub", () => + { + InputManager.Click(MouseButton.Left); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("slider is still at 1", () => slider.Current.Value, () => Is.EqualTo(1)); + } } } From 96437c4518a394c2354853b95c5f07853c6340d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 12:03:35 +0200 Subject: [PATCH 2015/2100] Fix tests specifying float precision for double bindable Would cause assignments to `.Value` to become imprecise even if the underlying bindable could represent the value 100% accurately. --- .../Visual/UserInterface/TestSceneRoundedSliderBar.cs | 2 +- .../Visual/UserInterface/TestSceneShearedSliderBar.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs index ae52c26fd2..311034d595 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.UserInterface private readonly BindableDouble current = new BindableDouble(5) { - Precision = 0.1f, + Precision = 0.1, MinValue = 0, MaxValue = 15 }; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs index 334fc4563a..a5072b4e60 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.UserInterface private readonly BindableDouble current = new BindableDouble(5) { - Precision = 0.1f, + Precision = 0.1, MinValue = 0, MaxValue = 15 }; From 89fec95b016364d2a97a30a1d6cbbbf6e46e19a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 12:11:25 +0200 Subject: [PATCH 2016/2100] Fix cross-test data dependency by recreating slider every time --- .../TestSceneRoundedSliderBar.cs | 22 +++++++++---------- .../TestSceneShearedSliderBar.cs | 22 +++++++++---------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs index 311034d595..66d54c8562 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs @@ -18,26 +18,24 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple); - private readonly BindableDouble current = new BindableDouble(5) - { - Precision = 0.1, - MinValue = 0, - MaxValue = 15 - }; - private RoundedSliderBar slider = null!; - [BackgroundDependencyLoader] - private void load() + [SetUpSteps] + public void SetUpSteps() { - Child = slider = new RoundedSliderBar + AddStep("create slider", () => Child = slider = new RoundedSliderBar { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Current = current, + Current = new BindableDouble(5) + { + Precision = 0.1, + MinValue = 0, + MaxValue = 15 + }, RelativeSizeAxes = Axes.X, Width = 0.4f - }; + }); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs index a5072b4e60..c3038ddb3d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs @@ -18,26 +18,24 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple); - private readonly BindableDouble current = new BindableDouble(5) - { - Precision = 0.1, - MinValue = 0, - MaxValue = 15 - }; - private ShearedSliderBar slider = null!; - [BackgroundDependencyLoader] - private void load() + [SetUpSteps] + public void SetUpSteps() { - Child = slider = new ShearedSliderBar + AddStep("create slider", () => Child = slider = new ShearedSliderBar { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Current = current, + Current = new BindableDouble(5) + { + Precision = 0.1, + MinValue = 0, + MaxValue = 15 + }, RelativeSizeAxes = Axes.X, Width = 0.4f - }; + }); } [Test] From 3b9c4c9d530eeee26f29a920299b0b363cd7348c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 12:04:27 +0200 Subject: [PATCH 2017/2100] Do not revert to default value when double-clicking disabled slider Closes https://github.com/ppy/osu/issues/25228. --- osu.Game/Graphics/UserInterface/RoundedSliderBar.cs | 6 +++++- osu.Game/Graphics/UserInterface/ShearedSliderBar.cs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs index e5976fe893..0981881ead 100644 --- a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs @@ -98,7 +98,11 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.TopCentre, RelativePositionAxes = Axes.X, Current = { Value = true }, - OnDoubleClicked = () => Current.SetDefault(), + OnDoubleClicked = () => + { + if (!Current.Disabled) + Current.SetDefault(); + }, }, }, hoverClickSounds = new HoverClickSounds() diff --git a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs index 9ef5f3073a..60a6670492 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs @@ -101,7 +101,11 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.TopCentre, RelativePositionAxes = Axes.X, Current = { Value = true }, - OnDoubleClicked = () => Current.SetDefault(), + OnDoubleClicked = () => + { + if (!Current.Disabled) + Current.SetDefault(); + }, }, }, hoverClickSounds = new HoverClickSounds() From dbb69419e6d49e6661d31eb22f2445e499e5e742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 12:39:07 +0200 Subject: [PATCH 2018/2100] Add test coverage for parsing new online ID --- .../Beatmaps/Formats/LegacyScoreDecoderTest.cs | 14 ++++++++++++++ .../Replays/taiko-replay-with-new-online-id.osr | Bin 0 -> 1531 bytes 2 files changed, 14 insertions(+) create mode 100644 osu.Game.Tests/Resources/Replays/taiko-replay-with-new-online-id.osr diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index c7fd3ba098..ab88be1511 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -101,6 +101,20 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestDecodeNewOnlineID() + { + var decoder = new TestLegacyScoreDecoder(); + + using (var resourceStream = TestResources.OpenResource("Replays/taiko-replay-with-new-online-id.osr")) + { + var score = decoder.Parse(resourceStream); + + Assert.That(score.ScoreInfo.OnlineID, Is.EqualTo(258)); + Assert.That(score.ScoreInfo.LegacyOnlineID, Is.EqualTo(-1)); + } + } + [TestCase(3, true)] [TestCase(6, false)] [TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)] diff --git a/osu.Game.Tests/Resources/Replays/taiko-replay-with-new-online-id.osr b/osu.Game.Tests/Resources/Replays/taiko-replay-with-new-online-id.osr new file mode 100644 index 0000000000000000000000000000000000000000..63e05f5fcdbda51e8f7c3322def87f4fc74f8da4 GIT binary patch literal 1531 zcmVvTrHZVD6F=Go4b8ul} zWo=<@Utx4?VRJGIAU8K+FfnCiFg9f~Ic8-uIb%6xWnyJxW-~W5FgY}2Hc0>o00001 z0000000e#v08sz|000003jk|tWgGt0+X(9f003P803ZOTO#lD@000007K}rXlJKRA z7M`&HOhZfu53FrP?1?N(Ce<_OIe53onYYKWQKjB!yy__GNTqRxRs4t|7d^=w?^a2; z=;p=RN*NtHSfdZ7KM{`CJ^MjsRn>7amAkGdpDK+x=PDo#tk4aBL?k%f)$Ebh0t%Nn zphxu;)QK_xDZ_`29#Slz$-BsTX6oS*tEpL9Ev7g9I5ScQ++9#K`PS+Bg7YLtZ zZhi6@^BMY%^-@>8#b8diGT>rK8W$eYWM@f+i3@MI*x2jwMoDv7i9dcH+Blva=xLAQ zQ+tf$l}n5c>1hLZYL!1qq`!()(I1Bi(teRO5k6sX_aTq*mVFs>aNFfP7QeOX;b~w1IAj)Te-(%eMdm^#T`gS4`h= z`(YRBu5x52OkrvAE;$$5Ka1rR+V@w)I!3+SveB0rxD~l$s=FEk0DPJXI01( zX<*T#vp20$0MeFEDmFU3$xU;pcASo(q2) zN#UlWQX+|aTArDrD&&we-Q#@5I>P@U#aZ7E_WVu}QZ z{u?U)bPE60zy8;&*@>H#nKF^riT4w%Ra_eUgCD(pFv%H+A(z{%SB~Z zD*JNyOplwu9x#_rabI#%4~0{9EZmm)IZ%cF!fh>HiAX@d}Izf*u8yw}?8+ zNnZ9YLWQcYNC*QlO6tVSziS_G!(qvFc@44`+vV{%9koFdUqvujoNp;)_jO1RN`{^0 zg=z?MuejP`10NKa5$Cxp-Se8FTJyl!tovh4wVKlspX4~-i8~TINGZ^#);*P#i)SE8 zzW1JsdM+A;?~>4&l@303`MAkXiv{_WIWc4$HYYGgMxnMFNGc{X2NX~R?2b@ZRs9cq z$!to>PZGzdmXRl&AW$UR-r`TGqkjmloZEFshw0Jkawz&1Ok4MmYz+~Jhh=obE&;f{ z+ZS(WELeZ_C2m3BkbxZ&DAUVh89w}bvS8+#qHjecQMK`PKw!@~opfX|DAuPffrNhq zDizxF1oWO4uo1ML<2%wPOFapwVrJdCB~~#Tt?D*NdN!;|NsC0|Ahbm z09^n8AOPI}000000000zf`AtsFAk^B)WLFNAyG7WV$N8-!j+hPu8$we_e;|#*+iNK z$kRN=q&Sl0(d`|*oEU1|*~5eG@dH3Miy0i*F=j=X+_a<6T3%2U@VpmXv8`-JhNX1! hacf^hACxf}U(1HP^l0fR;XoK|ePzq5Xz7uby&!FJ#7qDH literal 0 HcmV?d00001 From 8b9b085ef5422976b6ec9ef8cd11406c235209fc Mon Sep 17 00:00:00 2001 From: Termincc <112787855+Termincc@users.noreply.github.com> Date: Thu, 26 Oct 2023 22:15:10 +1000 Subject: [PATCH 2019/2100] Address mod incompatibilities Makes FreezeFrame and Transform mods incompatible. --- osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs index 0a1aab9ef1..89a00d6c32 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override LocalisableString Description => "Burn the notes into your memory."; //Alters the transforms of the approach circles, breaking the effects of these mods. - public override Type[] IncompatibleMods => new[] { typeof(OsuModApproachDifferent) }; + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform) }).ToArray(); public override ModType Type => ModType.Fun; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs index 2354cd50ae..92a499e735 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; @@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override LocalisableString Description => "Everything rotates. EVERYTHING."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel) }; + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame) }).ToArray(); private float theta; From 238e8175ae3b2ddb8e8b86dc969ddba2225e693f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Oct 2023 21:26:26 +0900 Subject: [PATCH 2020/2100] Add ability to quick retry using Ctrl-R Matches osu!stable --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 20220d88cd..947cd5f54f 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -149,6 +149,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene), new KeyBinding(InputKey.ExtraMouseButton2, GlobalAction.SkipCutscene), new KeyBinding(InputKey.Tilde, GlobalAction.QuickRetry), + new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.QuickRetry), new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit), new KeyBinding(new[] { InputKey.F3 }, GlobalAction.DecreaseScrollSpeed), new KeyBinding(new[] { InputKey.F4 }, GlobalAction.IncreaseScrollSpeed), From 526ee6e14070a2ba0d09ad544353be52796e698c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 14:46:24 +0200 Subject: [PATCH 2021/2100] Remove `IScoreInfo : IHasNamedFiles` inheritance --- osu.Game/Database/IHasRealmFiles.cs | 6 ++++-- osu.Game/Scoring/IScoreInfo.cs | 2 +- osu.Game/Scoring/ScoreInfo.cs | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/IHasRealmFiles.cs b/osu.Game/Database/IHasRealmFiles.cs index 79ea719583..b301bb04de 100644 --- a/osu.Game/Database/IHasRealmFiles.cs +++ b/osu.Game/Database/IHasRealmFiles.cs @@ -10,13 +10,15 @@ namespace osu.Game.Database /// /// A model that contains a list of files it is responsible for. /// - public interface IHasRealmFiles + public interface IHasRealmFiles : IHasNamedFiles { /// /// Available files in this model, with locally filenames. /// When performing lookups, consider using or to do case-insensitive lookups. /// - IList Files { get; } + new IList Files { get; } + + IEnumerable IHasNamedFiles.Files => Files; /// /// A combined hash representing the model, based on the files it contains. diff --git a/osu.Game/Scoring/IScoreInfo.cs b/osu.Game/Scoring/IScoreInfo.cs index 4083d57fa0..cde48c3be3 100644 --- a/osu.Game/Scoring/IScoreInfo.cs +++ b/osu.Game/Scoring/IScoreInfo.cs @@ -9,7 +9,7 @@ using osu.Game.Users; namespace osu.Game.Scoring { - public interface IScoreInfo : IHasOnlineID, IHasNamedFiles + public interface IScoreInfo : IHasOnlineID { IUser User { get; } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 6b03e876c4..722d83cac8 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -187,7 +187,6 @@ namespace osu.Game.Scoring IRulesetInfo IScoreInfo.Ruleset => Ruleset; IBeatmapInfo? IScoreInfo.Beatmap => BeatmapInfo; IUser IScoreInfo.User => User; - IEnumerable IHasNamedFiles.Files => Files; #region Properties required to make things work with existing usages From 900530080ff7f4c33a19b6107214c80ff4f0f997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 14:56:22 +0200 Subject: [PATCH 2022/2100] Make `SoloScoreInfo` implement `IScoreInfo` --- .../API/Requests/Responses/SoloScoreInfo.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs index 783522220b..0e31f11dc1 100644 --- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs @@ -7,16 +7,16 @@ using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Users; namespace osu.Game.Online.API.Requests.Responses { [Serializable] - public class SoloScoreInfo : IHasOnlineID + public class SoloScoreInfo : IScoreInfo { [JsonProperty("beatmap_id")] public int BeatmapID { get; set; } @@ -138,6 +138,18 @@ namespace osu.Game.Online.API.Requests.Responses #endregion + #region IScoreInfo + + public long OnlineID => (long?)ID ?? -1; + + IUser IScoreInfo.User => User!; + DateTimeOffset IScoreInfo.Date => EndedAt; + long IScoreInfo.LegacyOnlineID => (long?)LegacyScoreId ?? -1; + IBeatmapInfo IScoreInfo.Beatmap => Beatmap!; + IRulesetInfo IScoreInfo.Ruleset => Beatmap!.Ruleset; + + #endregion + public override string ToString() => $"score_id: {ID} user_id: {UserID}"; /// @@ -223,7 +235,5 @@ namespace osu.Game.Online.API.Requests.Responses Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), }; - - public long OnlineID => (long?)ID ?? -1; } } From c3e9f5184f504f119867c3745af730c6fafe02de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 15:09:34 +0200 Subject: [PATCH 2023/2100] Fix `SoloScoreInfo` not copying over legacy score ID when converting to `ScoreInfo` --- osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs index 0e31f11dc1..ac2d8152b1 100644 --- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs @@ -190,6 +190,7 @@ namespace osu.Game.Online.API.Requests.Responses var score = new ScoreInfo { OnlineID = OnlineID, + LegacyOnlineID = (long?)LegacyScoreId ?? -1, User = User ?? new APIUser { Id = UserID }, BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID }, Ruleset = new RulesetInfo { OnlineID = RulesetID }, From cbb2a0dd70ddce70e2e156be7bac632bbc9b6480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 15:09:59 +0200 Subject: [PATCH 2024/2100] Use both score ID types to deduplicate score on solo results screen --- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index f187b8a302..da08a26a58 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Solo; @@ -67,7 +68,7 @@ namespace osu.Game.Screens.Ranking return null; getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); - getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineID != Score.OnlineID).Select(s => s.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo))); + getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => !s.MatchesOnlineID(Score)).Select(s => s.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo))); return getScoreRequest; } From 359ae3120494d0b9d3b61599d6f50f5abe0330ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 15:40:46 +0200 Subject: [PATCH 2025/2100] Fix catch distance snap grid not moving Regressed in https://github.com/ppy/osu/pull/25154. Specifically, in 013b5fa916d819ec7a8a93b1692d4aa027934a67 and 74b86349d58e5169e52bbdc70cef3dec81579a74. A simple case of too-much-code-deleted-itis. --- .../Edit/CatchHitObjectComposer.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 6f0ee260ab..4172720ada 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; @@ -179,5 +180,33 @@ namespace osu.Game.Rulesets.Catch.Edit return null; } } + + protected override void Update() + { + base.Update(); + + updateDistanceSnapGrid(); + } + + private void updateDistanceSnapGrid() + { + if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True) + { + distanceSnapGrid.Hide(); + return; + } + + var sourceHitObject = getDistanceSnapGridSourceHitObject(); + + if (sourceHitObject == null) + { + distanceSnapGrid.Hide(); + return; + } + + distanceSnapGrid.Show(); + distanceSnapGrid.StartTime = sourceHitObject.GetEndTime(); + distanceSnapGrid.StartX = sourceHitObject.EffectiveX; + } } } From 79910df9593b8478419b4eb2f4be12b0cfd1dbf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 15:46:32 +0200 Subject: [PATCH 2026/2100] Fix catch distance snap provider not hiding slider properly Regressed in https://github.com/ppy/osu/pull/25171. The old code was kinda dependent on correct order of setting `Disabled`. `CatchHitObjectComposer` would disable distance spacing in its BDL, and then via the base `DistancedHitObjectComposer.LoadComplete()`, the slider would be faded out. The switch to composition broke that ordering. To fix, stop relying on ordering and just respond to changes as they come. That's what bindables are for. --- osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index 0b1809e7d9..ddf539771d 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -98,12 +98,6 @@ namespace osu.Game.Rulesets.Edit } }); - if (DistanceSpacingMultiplier.Disabled) - { - distanceSpacingSlider.Hide(); - return; - } - DistanceSpacingMultiplier.Value = editorBeatmap.BeatmapInfo.DistanceSpacing; DistanceSpacingMultiplier.BindValueChanged(multiplier => { @@ -116,6 +110,8 @@ namespace osu.Game.Rulesets.Edit editorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue; }, true); + DistanceSpacingMultiplier.BindDisabledChanged(disabled => distanceSpacingSlider.Alpha = disabled ? 0 : 1, true); + // Manual binding to handle enabling distance spacing when the slider is interacted with. distanceSpacingSlider.Current.BindValueChanged(spacing => { From 5d6a58d443cf6467bcc72b8db4d36875acbab7bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 16:19:53 +0200 Subject: [PATCH 2027/2100] Add failing test scene for scroll handling in song select --- .../Navigation/TestSceneScreenNavigation.cs | 38 +++++++++++++++++++ osu.Game/Screens/Select/SongSelect.cs | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index fa1ebf5c56..a6d4fb0b52 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -7,6 +7,7 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -16,6 +17,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Configuration; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Leaderboards; @@ -34,6 +36,7 @@ using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Options; using osu.Game.Tests.Beatmaps.IO; @@ -165,6 +168,41 @@ namespace osu.Game.Tests.Visual.Navigation ConfirmAtMainMenu(); } + [Test] + public void TestSongSelectScrollHandling() + { + TestPlaySongSelect songSelect = null; + double scrollPosition = 0; + + AddStep("set game volume to max", () => Game.Dependencies.Get().SetValue(FrameworkSetting.VolumeUniversal, 1d)); + AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("store scroll position", () => scrollPosition = getCarouselScrollPosition()); + + AddStep("move to left side", () => InputManager.MoveMouseTo( + songSelect.ChildrenOfType().Single().ScreenSpaceDrawQuad.TopLeft + new Vector2(1))); + AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1)); + AddAssert("carousel didn't move", getCarouselScrollPosition, () => Is.EqualTo(scrollPosition)); + + AddRepeatStep("alt-scroll down", () => + { + InputManager.PressKey(Key.AltLeft); + InputManager.ScrollVerticalBy(-1); + InputManager.ReleaseKey(Key.AltLeft); + }, 5); + AddAssert("game volume decreased", () => Game.Dependencies.Get().Get(FrameworkSetting.VolumeUniversal), () => Is.LessThan(1)); + + AddStep("move to carousel", () => InputManager.MoveMouseTo(songSelect.ChildrenOfType().Single())); + AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1)); + AddAssert("carousel moved", getCarouselScrollPosition, () => Is.Not.EqualTo(scrollPosition)); + + double getCarouselScrollPosition() => Game.ChildrenOfType>().Single().Current; + } + /// /// This tests that the F1 key will open the mod select overlay, and not be handled / blocked by the music controller (which has the same default binding /// but should be handled *after* song select). diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index d5ec94ad71..827884f971 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -1019,7 +1019,7 @@ namespace osu.Game.Screens.Select /// /// Handles mouse interactions required when moving away from the carousel. /// - private partial class LeftSideInteractionContainer : Container + internal partial class LeftSideInteractionContainer : Container { private readonly Action? resetCarouselPosition; From 2fa221738184c9ded7f091cea16bf40f4e02765d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 16:26:31 +0200 Subject: [PATCH 2028/2100] Fix left side of carousel blocking volume adjust hotkeys Closes https://github.com/ppy/osu/issues/25234. A little ad-hoc, but probably fine...? --- osu.Game/Screens/Select/SongSelect.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 827884f971..dfea4e3794 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -1028,7 +1028,10 @@ namespace osu.Game.Screens.Select this.resetCarouselPosition = resetCarouselPosition; } - protected override bool OnScroll(ScrollEvent e) => true; + // we want to block plain scrolls on the left side so that they don't scroll the carousel, + // but also we *don't* want to handle scrolls when they're combined with keyboard modifiers + // as those will usually correspond to other interactions like adjusting volume. + protected override bool OnScroll(ScrollEvent e) => !e.ControlPressed && !e.AltPressed && !e.ShiftPressed && !e.SuperPressed; protected override bool OnMouseDown(MouseDownEvent e) => true; From 0482c05d7c031132a17e5e0fa3e4b605051cd2b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 19:27:05 +0200 Subject: [PATCH 2029/2100] Add failing test case --- .../TestSceneMasterGameplayClockContainer.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs index 393217f371..1368b42a3c 100644 --- a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs +++ b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs @@ -108,6 +108,28 @@ namespace osu.Game.Tests.Gameplay AddAssert("gameplay clock time = 10000", () => gameplayClockContainer.CurrentTime, () => Is.EqualTo(10000).Within(10f)); } + [Test] + public void TestStopUsingBeatmapClock() + { + ClockBackedTestWorkingBeatmap working = null; + MasterGameplayClockContainer gameplayClockContainer = null; + BindableDouble frequencyAdjustment = new BindableDouble(2); + + AddStep("create container", () => + { + working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio); + Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0); + + gameplayClockContainer.Reset(startClock: true); + }); + + AddStep("apply frequency adjustment", () => gameplayClockContainer.AdjustmentsFromMods.AddAdjustment(AdjustableProperty.Frequency, frequencyAdjustment)); + AddAssert("track frequency changed", () => working.Track.AggregateFrequency.Value, () => Is.EqualTo(2)); + + AddStep("stop using beatmap clock", () => gameplayClockContainer.StopUsingBeatmapClock()); + AddAssert("frequency adjustment unapplied", () => working.Track.AggregateFrequency.Value, () => Is.EqualTo(1)); + } + protected override void Dispose(bool isDisposing) { localConfig?.Dispose(); From 565ae99e0dfddbecef6bc24828ba6c39c837f5cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 19:27:50 +0200 Subject: [PATCH 2030/2100] Fix `StopUsingBeatmapClock()` applying adjustments to track it was supposed to stop using - Closes https://github.com/ppy/osu/issues/25248 - Possibly also closes https://github.com/ppy/osu/issues/20475 Regressed in e33486a766044c17c2f254f5e8df6d72b29c341e. `StopUsingBeatmapClock()` intends to, as the name says, stop operating on the working beatmap clock to yield its usage to other components on exit. As part of that it tries to unapply audio adjustments so that other screens can apply theirs freely instead. However, the aforementioned commit introduced a bug in this. Previously to it, `track` was an alias for the `SourceClock`, which could be mutated in an indirect way via `ChangeSource()` calls. The aforementioned commit made `track` a `readonly` field, initialised in constructor, which would _never_ change value. In particular, it would _always_ be the beatmap track, which meant that `StopUsingBeatmapClock()` would remove the adjustments from the beatmap track, but then at the end of the method, _apply them onto that same track again_. This was only saved by the fact that clock adjustments are removed again on disposal of the `MasterGameplayClockContainer()`. This - due to async disposal pressure - could explain infrequently reported cases wherein the track would just continue to speed up ad infinitum. To fix, fully substitute the beatmap track for a virtual track at the point of calling `StopUsingBeatmapClock()`. --- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 70d9ecd3e7..6e07b01b5f 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play private readonly WorkingBeatmap beatmap; - private readonly Track track; + private Track track; private readonly double skipTargetTime; @@ -188,11 +188,11 @@ namespace osu.Game.Screens.Play { removeSourceClockAdjustments(); - var virtualTrack = new TrackVirtual(beatmap.Track.Length); - virtualTrack.Seek(CurrentTime); + track = new TrackVirtual(beatmap.Track.Length); + track.Seek(CurrentTime); if (IsRunning) - virtualTrack.Start(); - ChangeSource(virtualTrack); + track.Start(); + ChangeSource(track); addSourceClockAdjustments(); } From fdb81bfa4ca2d9819bb0c252059b7b84919af583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 19:38:41 +0200 Subject: [PATCH 2031/2100] Rename methods to not mention "source clock" anymore --- .../Screens/Play/MasterGameplayClockContainer.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 6e07b01b5f..1c860e9d4b 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -145,7 +145,7 @@ namespace osu.Game.Screens.Play protected override void StartGameplayClock() { - addSourceClockAdjustments(); + addAdjustmentsToTrack(); base.StartGameplayClock(); @@ -186,7 +186,7 @@ namespace osu.Game.Screens.Play /// public void StopUsingBeatmapClock() { - removeSourceClockAdjustments(); + removeAdjustmentsFromTrack(); track = new TrackVirtual(beatmap.Track.Length); track.Seek(CurrentTime); @@ -194,12 +194,12 @@ namespace osu.Game.Screens.Play track.Start(); ChangeSource(track); - addSourceClockAdjustments(); + addAdjustmentsToTrack(); } private bool speedAdjustmentsApplied; - private void addSourceClockAdjustments() + private void addAdjustmentsToTrack() { if (speedAdjustmentsApplied) return; @@ -213,7 +213,7 @@ namespace osu.Game.Screens.Play speedAdjustmentsApplied = true; } - private void removeSourceClockAdjustments() + private void removeAdjustmentsFromTrack() { if (!speedAdjustmentsApplied) return; @@ -228,7 +228,7 @@ namespace osu.Game.Screens.Play protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - removeSourceClockAdjustments(); + removeAdjustmentsFromTrack(); } ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.Beatmap.ControlPointInfo; From 24b1d1e9558badb2f1a6cb5dc30a5aa6fc79f41d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Oct 2023 18:18:07 +0900 Subject: [PATCH 2032/2100] Fix code quality fail --- osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs index 89a00d6c32..f1197ce0cd 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override LocalisableString Description => "Burn the notes into your memory."; //Alters the transforms of the approach circles, breaking the effects of these mods. - public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform) }).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform) }).ToArray(); public override ModType Type => ModType.Fun; From f931f4c324f96063a3964fc660daf06d99e657d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Oct 2023 18:19:44 +0900 Subject: [PATCH 2033/2100] Remove reundant interface specification --- osu.Game/Skinning/SkinInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index c2b80b7ead..9763d3b57e 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -14,7 +14,7 @@ namespace osu.Game.Skinning { [MapTo("Skin")] [JsonObject(MemberSerialization.OptIn)] - public class SkinInfo : RealmObject, IHasRealmFiles, IEquatable, IHasGuidPrimaryKey, ISoftDelete, IHasNamedFiles + public class SkinInfo : RealmObject, IHasRealmFiles, IEquatable, IHasGuidPrimaryKey, ISoftDelete { internal static readonly Guid TRIANGLES_SKIN = new Guid("2991CFD8-2140-469A-BCB9-2EC23FBCE4AD"); internal static readonly Guid ARGON_SKIN = new Guid("CFFA69DE-B3E3-4DEE-8563-3C4F425C05D0"); From 7140eee870bab0d86ab25c15f3d56dce1b68d627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 11:38:10 +0200 Subject: [PATCH 2034/2100] Add failing test coverage for quick retry after completion not changing rank --- osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index a6d4fb0b52..2f378917e6 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -259,6 +259,7 @@ namespace osu.Game.Tests.Visual.Navigation var getOriginalPlayer = playToCompletion(); AddStep("attempt to retry", () => getOriginalPlayer().ChildrenOfType().First().Action()); + AddAssert("original play isn't failed", () => getOriginalPlayer().Score.ScoreInfo.Rank, () => Is.Not.EqualTo(ScoreRank.F)); AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player); } From 86a8ab6db6548d211e5f0e5ff1a4feb4b7a506c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 11:39:43 +0200 Subject: [PATCH 2035/2100] Fix quick retry immediately after completion marking score as failed Closes https://github.com/ppy/osu/issues/25247. The scenario involved here is as follows: 1. User completes beatmap by hitting all notes. 2. `checkScoreCompleted()` determines completion, and calls `progressToResults(withDelay: true)`. 3. `progressToResults()` schedules `resultsDisplayDelegate`, which includes a call to `prepareAndImportScoreAsync()`, a second in the future. 4. User presses quick retry hotkey. This calls `Player.Restart(quickRestart: true)`, which invokes `Player.RestartRequested`, which in turn calls `PlayerLoader.restartRequested(true)`, which in turn causes `PlayerLoader` to make itself current, which means that `Player.OnExiting()` will get called. 5. `Player.OnExiting()` sees that `prepareScoreForDisplayTask` is null (because `prepareAndImportScoreAsync()` - which sets it - is scheduled to happen in the future), and as such assumes that the score did not complete. Thus, it marks the score as failed. 6. `Player.Restart()` after invoking `RestartRequested` calls `PerformExit(false)`, which then will unconditionally call `prepareAndImportScoreAsync()`. But the score has already been marked as failed. The flow above can be described as "convoluted", but I'm not sure I have it in me right now to try and refactor it again. Therefore, to fix, switch the `prepareScoreForDisplayTask` null check in `Player.OnExiting()` to check `GameplayState.HasPassed` instead, as it is not susceptible to the same out-of-order read issue. --- osu.Game/Screens/Play/Player.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 97bfa35d49..18d0a65d7a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1115,8 +1115,7 @@ namespace osu.Game.Screens.Play if (!GameplayState.HasPassed && !GameplayState.HasFailed) GameplayState.HasQuit = true; - // if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap. - if (prepareScoreForDisplayTask == null && DrawableRuleset.ReplayScore == null) + if (!GameplayState.HasPassed && DrawableRuleset.ReplayScore == null) ScoreProcessor.FailScore(Score.ScoreInfo); } From 3944b045ed4582b233066dc714be6075b47482f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 11:58:02 +0200 Subject: [PATCH 2036/2100] Add extra test coverage for marking score as failed --- osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 2f378917e6..7fa4f8c836 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -247,6 +247,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("end spectator before retry", () => Game.SpectatorClient.EndPlaying(player.GameplayState)); AddStep("attempt to retry", () => player.ChildrenOfType().First().Action()); + AddAssert("old player score marked failed", () => player.Score.ScoreInfo.Rank, () => Is.EqualTo(ScoreRank.F)); AddUntilStep("wait for old player gone", () => Game.ScreenStack.CurrentScreen != player); AddUntilStep("get new player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); From 96d784e06bd6068f09620eaaa45c4766d0da900d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 12:39:54 +0200 Subject: [PATCH 2037/2100] Delete `ScoreInfo.HasReplay` as no longer needed --- osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs | 2 +- osu.Game/Scoring/IScoreInfo.cs | 2 -- osu.Game/Scoring/ScoreInfo.cs | 2 -- osu.Game/Screens/Ranking/ReplayDownloadButton.cs | 2 +- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index 6ccf73d8ff..5b32f380b9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.Gameplay OnlineID = hasOnlineId ? online_score_id : 0, Ruleset = new OsuRuleset().RulesetInfo, BeatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(), - Hash = replayAvailable ? "online" : string.Empty, + HasOnlineReplay = replayAvailable, User = new APIUser { Id = 39828, diff --git a/osu.Game/Scoring/IScoreInfo.cs b/osu.Game/Scoring/IScoreInfo.cs index cde48c3be3..a1d076b8c2 100644 --- a/osu.Game/Scoring/IScoreInfo.cs +++ b/osu.Game/Scoring/IScoreInfo.cs @@ -22,8 +22,6 @@ namespace osu.Game.Scoring double Accuracy { get; } - bool HasReplay { get; } - long LegacyOnlineID { get; } DateTimeOffset Date { get; } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 722d83cac8..d712702331 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -94,8 +94,6 @@ namespace osu.Game.Scoring public double Accuracy { get; set; } - public bool HasReplay => !string.IsNullOrEmpty(Hash) || HasOnlineReplay; - [Ignored] public bool HasOnlineReplay { get; set; } diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index b6166e97f6..df5f9c7a8a 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Ranking if (State.Value == DownloadState.LocallyAvailable) return ReplayAvailability.Local; - if (Score.Value?.HasReplay == true) + if (Score.Value?.HasOnlineReplay == true) return ReplayAvailability.Online; return ReplayAvailability.NotAvailable; From 32fc19ea0d2515aeab4e84248f6cf67ec7d7de98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 13:22:17 +0200 Subject: [PATCH 2038/2100] Fix results screen test failure --- 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 146482e6fb..ab2e867255 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -362,7 +362,7 @@ namespace osu.Game.Tests.Visual.Ranking { var score = TestResources.CreateTestScoreInfo(); score.TotalScore += 10 - i; - score.Hash = $"test{i}"; + score.HasOnlineReplay = true; scores.Add(score); } From 2d5b1711f6ffc233ca308153a665d18dcbbccea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 13:27:48 +0200 Subject: [PATCH 2039/2100] Share `!HasPassed` condition --- osu.Game/Screens/Play/Player.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 18d0a65d7a..a1ec0b3167 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1110,12 +1110,12 @@ namespace osu.Game.Screens.Play failAnimationContainer?.Stop(); PauseOverlay?.StopAllSamples(); - if (LoadedBeatmapSuccessfully) + if (LoadedBeatmapSuccessfully && !GameplayState.HasPassed) { - if (!GameplayState.HasPassed && !GameplayState.HasFailed) + if (!GameplayState.HasFailed) GameplayState.HasQuit = true; - if (!GameplayState.HasPassed && DrawableRuleset.ReplayScore == null) + if (DrawableRuleset.ReplayScore == null) ScoreProcessor.FailScore(Score.ScoreInfo); } From dc7f5cd6edc94275a56b3b5cdb5766c76c84181c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 13:30:51 +0200 Subject: [PATCH 2040/2100] Add preventive assertions concerning submission flow state --- osu.Game/Screens/Play/Player.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a1ec0b3167..58c8c3389a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1112,6 +1112,8 @@ namespace osu.Game.Screens.Play if (LoadedBeatmapSuccessfully && !GameplayState.HasPassed) { + Debug.Assert(resultsDisplayDelegate == null && prepareScoreForDisplayTask == null); + if (!GameplayState.HasFailed) GameplayState.HasQuit = true; From 6789a522d6d79d6fd1d4f8c15b9c83b119ab61b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 14:15:30 +0200 Subject: [PATCH 2041/2100] Rename test to distinguish it from test-to-come --- .../Visual/Navigation/TestSceneSkinEditorNavigation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index 88904bf85b..d08d53f747 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Navigation private SkinEditor skinEditor => Game.ChildrenOfType().FirstOrDefault(); [Test] - public void TestEditComponentDuringGameplay() + public void TestEditComponentFromGameplayScene() { advanceToSongSelect(); openSkinEditor(); From b5cb5380045b0d8be6031cbdffb4a799ac402bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 14:23:41 +0200 Subject: [PATCH 2042/2100] Add failing test case for skin editor freeze --- .../TestSceneSkinEditorNavigation.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index d08d53f747..c17a9ddf5f 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -5,6 +5,7 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -18,6 +19,7 @@ using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD.HitErrorMeters; +using osu.Game.Skinning; using osu.Game.Tests.Beatmaps.IO; using osuTK; using osuTK.Input; @@ -69,6 +71,28 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default); } + [Test] + public void TestMutateProtectedSkinDuringGameplay() + { + advanceToSongSelect(); + AddStep("set default skin", () => Game.Dependencies.Get().CurrentSkinInfo.SetDefault()); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("enable NF", () => Game.SelectedMods.Value = new[] { new OsuModNoFail() }); + AddStep("enter gameplay", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + return Game.ScreenStack.CurrentScreen is Player; + }); + + openSkinEditor(); + AddUntilStep("current skin is mutable", () => !Game.Dependencies.Get().CurrentSkin.Value.SkinInfo.Value.Protected); + } + [Test] public void TestComponentsDeselectedOnSkinEditorHide() { From 5ad962070c0448b81c8457b8816818b6e6041502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 14:34:30 +0200 Subject: [PATCH 2043/2100] Fix skin editor freezing game if opened during active gameplay --- osu.Game/Database/ImportParameters.cs | 6 ++++++ osu.Game/Database/RealmArchiveModelImporter.cs | 6 +++--- osu.Game/Skinning/SkinManager.cs | 5 ++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/ImportParameters.cs b/osu.Game/Database/ImportParameters.cs index 83ca0ac694..8d37597afc 100644 --- a/osu.Game/Database/ImportParameters.cs +++ b/osu.Game/Database/ImportParameters.cs @@ -21,5 +21,11 @@ namespace osu.Game.Database /// Whether this import should use hard links rather than file copy operations if available. /// public bool PreferHardLinks { get; set; } + + /// + /// If set to , this import will not respect . + /// This is useful for cases where an import must complete even if gameplay is in progress. + /// + public bool ImportImmediately { get; set; } } } diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index 730465e1b0..5383040eb4 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -261,7 +261,7 @@ namespace osu.Game.Database /// An optional cancellation token. public virtual Live? ImportModel(TModel item, ArchiveReader? archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) => Realm.Run(realm => { - pauseIfNecessary(cancellationToken); + pauseIfNecessary(parameters, cancellationToken); TModel? existing; @@ -560,9 +560,9 @@ namespace osu.Game.Database /// Whether to perform deletion. protected virtual bool ShouldDeleteArchive(string path) => false; - private void pauseIfNecessary(CancellationToken cancellationToken) + private void pauseIfNecessary(ImportParameters importParameters, CancellationToken cancellationToken) { - if (!PauseImports) + if (!PauseImports || importParameters.ImportImmediately) return; Logger.Log($@"{GetType().Name} is being paused."); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index ca46d3af0c..59c2a8bca0 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -182,7 +182,10 @@ namespace osu.Game.Skinning Name = NamingUtils.GetNextBestName(existingSkinNames, $@"{s.Name} (modified)") }; - var result = skinImporter.ImportModel(skinInfo); + var result = skinImporter.ImportModel(skinInfo, parameters: new ImportParameters + { + ImportImmediately = true // to avoid possible deadlocks when editing skin during gameplay. + }); if (result != null) { From 35f30d6135b29b73b68d861bb68564cc951f1a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 14:39:58 +0200 Subject: [PATCH 2044/2100] Scale back debug assertion The import preparation task can actually be non-null when exiting even if the player hasn't passed: - fail beatmap - click import button to import the failed replay --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 58c8c3389a..2392fd9f76 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1112,7 +1112,7 @@ namespace osu.Game.Screens.Play if (LoadedBeatmapSuccessfully && !GameplayState.HasPassed) { - Debug.Assert(resultsDisplayDelegate == null && prepareScoreForDisplayTask == null); + Debug.Assert(resultsDisplayDelegate == null); if (!GameplayState.HasFailed) GameplayState.HasQuit = true; From 7a5f3b856f41d3288b2d7940cb9009dabf8d8770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 19:43:49 +0200 Subject: [PATCH 2045/2100] Add visual test coverage for displaying hitcircles with high combo index --- .../Resources/special-skin/display-0@2x.png | Bin 0 -> 5085 bytes .../Resources/special-skin/display-1@2x.png | Bin 0 -> 1262 bytes .../Resources/special-skin/display-2@2x.png | Bin 0 -> 3534 bytes .../Resources/special-skin/display-3@2x.png | Bin 0 -> 3456 bytes .../Resources/special-skin/display-4@2x.png | Bin 0 -> 3213 bytes .../Resources/special-skin/display-5@2x.png | Bin 0 -> 3647 bytes .../Resources/special-skin/display-6@2x.png | Bin 0 -> 4954 bytes .../Resources/special-skin/display-7@2x.png | Bin 0 -> 2503 bytes .../Resources/special-skin/display-8@2x.png | Bin 0 -> 5710 bytes .../Resources/special-skin/display-9@2x.png | Bin 0 -> 5195 bytes .../Resources/special-skin/skin.ini | 1 + .../TestSceneHitCircle.cs | 10 ++++++---- 12 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-0@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-1@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-2@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-3@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-4@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-5@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-6@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-7@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-8@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-9@2x.png diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-0@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..67d2e2cf04a71c59957a79a69ef6de8e02a6bbb2 GIT binary patch literal 5085 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-1@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-1@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2df10655efabdb9eefbad32abd4be42deef4fb02 GIT binary patch literal 1262 zcmeAS@N?(olHy`uVBq!ia0vp^IzXJo!3HFcw{9)~QjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`ISV`@iy0XBc7rhE@gG;rfNGjOT^vIy;@-}&_Lp`PXb?Yi*oMiei6eZX zfU1z7(2@mSQC`1P_AA)8sp&2}q+9ADa#Dn?(LsUB;Y2`!gFxIQQK=JW?)7fIcf0!C z+nap9Jtu!%`@YQnSn+i03$r45E#T>A1DjarutZbLY;Te{PxcT!RyJ zZO@dCM0Z@jaos^q!A~K~@t5^`#;ikmY5SiP?ezEKPxkls-&0#x$Cs07uar}t)w!Uu zs=9jmgQrTVyJY_t9yB^|rf8PqHP*QodX>47&wet>5R2UIy8PBp6|Ul0KG&Kf4p$ti zef#RwEFu0^FJ2@#J3HqjBs6?)y?E`}&JQe??!9~R=G(VvckkTU@cQ*@tDF1w?bFQ= zdvWA5M|oM9-$OH56CyP%1t=SibYQ|{=?Z{M`((6s5(Zyr0wcHgbw9}rk2`tx>gYPtU; z^W-Xl=i(nZl(jMsh?(vgRw^U5fQT9IZw=nhS-=x$TCxXIM zJj4RIy4it_X}L2$WLlr|pA^CVf(!l`ULYyXvj!7e*YtSV1+^XR=gyp>BV>8FMKkha z#jNns&f2Hja!N|R$eO%QTyB$h+41h(yF7W8H}Bq+-5+jiVXG2z>dZZ7&-#Yxh(#_8c(DHb z`SU;j?dsU1#gsi`0{g_QQBNo6Yb>nm|RUf(A7WbUDqjcdbJzxnuaV*YETetl)G zcqa?d_d>4I3<{?T99Pde6fpnC>II=Ylyn^bG~H;@X6cX7JHB}N^5r~%NB@R4FLj)t z6BK^<-Q<@_N6b4|UH>%fFj4hp@vQcTa4~-2&K=+=#)~E3KRos__o(YBFc2m zR-H)}hnM}Y{-!)}gV(mytR1RGYbI@#{HC~-C;jN@vm4$S$A)Y%o?{v%DVF(bHOoaI zQy-H>I&pct>8)kF>Aj+6Bq855_J1#&y5qI<&~zbTV`Jl-va)IV>V917w#7zACj&D` zUca;5{BV8YzNYyfZrqTNmXo^{CD!=brQqk$j~_SQew^TPLuJ#4`ua;BmkOUdedfT= zf;hjJ#PgRrdBDs!_5!<0=N!F0iKYIjhm3<&w|-q<-OBExIz4|`-`mdZZe_Mq1eQ?@ Mp00i_>zopr0AYwh{{R30 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-2@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-2@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..eeb8ec0edf1953c6a8b036e6e33207bb99f84bf6 GIT binary patch literal 3534 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 IM6N<$f*~28E&u=k literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-3@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-3@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4ee73f503aa11548b1dc44347629007b288ceefb GIT binary patch literal 3456 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 A#sB~S literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-9@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-9@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..53b687fdea4d84005a44887e130fff5044936aa1 GIT binary patch literal 5195 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 FV1j9g503x< literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini index 49ac2cf80d..9d16267d73 100644 --- a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini +++ b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini @@ -1,3 +1,4 @@ [General] Version: latest HitCircleOverlayAboveNumber: 0 +HitCirclePrefix: display \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index af02087d1a..30b0451a3b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Hit Big Stream", () => SetContents(_ => testStream(2, true))); AddStep("Hit Medium Stream", () => SetContents(_ => testStream(5, true))); AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true))); + AddStep("High combo index", () => SetContents(_ => testSingle(2, true, comboIndex: 15))); } [Test] @@ -66,12 +67,12 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true))); } - private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null) + private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null, int comboIndex = 0) { var playfield = new TestOsuPlayfield(); for (double t = timeOffset; t < timeOffset + 60000; t += 2000) - playfield.Add(createSingle(circleSize, auto, t, positionOffset)); + playfield.Add(createSingle(circleSize, auto, t, positionOffset, comboIndex: comboIndex)); return playfield; } @@ -84,14 +85,14 @@ namespace osu.Game.Rulesets.Osu.Tests for (int i = 0; i <= 1000; i += 100) { - playfield.Add(createSingle(circleSize, auto, i, pos, hitOffset)); + playfield.Add(createSingle(circleSize, auto, i, pos, hitOffset, i / 100 - 1)); pos.X += 50; } return playfield; } - private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset, double hitOffset = 0) + private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset, double hitOffset = 0, int comboIndex = 0) { positionOffset ??= Vector2.Zero; @@ -99,6 +100,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + 1000 + timeOffset, Position = OsuPlayfield.BASE_SIZE / 4 + positionOffset.Value, + IndexInCurrentCombo = comboIndex, }; circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize }); From c9cb0561f7a2ac5117d9add6792e02b53014c1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 19:48:10 +0200 Subject: [PATCH 2046/2100] Move `maxSizePerGlyph` optional ctor param to init-only property I'm doing this as I'm about to add more similar properties to `LegacySpriteText` and I don't want to create a twenty-argument constructor monstrosity. --- .../Skinning/Legacy/OsuLegacySkinTransformer.cs | 3 ++- osu.Game/Skinning/LegacySpriteText.cs | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 1de8fefaae..c01d28c8e1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -145,10 +145,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; const float hitcircle_text_scale = 0.8f; - return new LegacySpriteText(LegacyFont.HitCircle, OsuHitObject.OBJECT_DIMENSIONS * 2 / hitcircle_text_scale) + return new LegacySpriteText(LegacyFont.HitCircle) { // stable applies a blanket 0.8x scale to hitcircle fonts Scale = new Vector2(hitcircle_text_scale), + MaxSizePerGlyph = OsuHitObject.OBJECT_DIMENSIONS * 2 / hitcircle_text_scale, }; case OsuSkinComponents.SpinnerBody: diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index 7eb92126fa..a803ef8747 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -12,8 +12,9 @@ namespace osu.Game.Skinning { public sealed partial class LegacySpriteText : OsuSpriteText { + public Vector2? MaxSizePerGlyph { get; init; } + private readonly LegacyFont font; - private readonly Vector2? maxSizePerGlyph; private LegacyGlyphStore glyphStore = null!; @@ -21,10 +22,9 @@ namespace osu.Game.Skinning protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x' }; - public LegacySpriteText(LegacyFont font, Vector2? maxSizePerGlyph = null) + public LegacySpriteText(LegacyFont font) { this.font = font; - this.maxSizePerGlyph = maxSizePerGlyph; Shadow = false; UseFullGlyphHeight = false; @@ -36,7 +36,7 @@ namespace osu.Game.Skinning Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: true); Spacing = new Vector2(-skin.GetFontOverlap(font), 0); - glyphStore = new LegacyGlyphStore(skin, maxSizePerGlyph); + glyphStore = new LegacyGlyphStore(skin, MaxSizePerGlyph); } protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore); From 99e590c8dd645c58b2fa6197e2f9bd7b003b9e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 20:10:36 +0200 Subject: [PATCH 2047/2100] Fix legacy sprite texts not matching stable with respect to fixed width stable's `pSpriteText` has a `TextConstantSpacing` flag, that is selectively enabled for some usages. In particular, these are: - mania combo counter (not yet implemented) - taiko combo counter (not yet implemented) - score counter - accuracy counter - scoreboard entries (not yet implemented) Everything else uses non-fixed-width fonts. Hilariously, `LegacySpinner` _tried_ to account for this by changing `Font` to have `fixedWidth: false` specified, only to fail to notice that `LegacySpriteText` changes `Font` in its BDL, making the property set do precisely nothing. For this reason, attempting to set `Font` on a `LegacySpriteText` will now throw. --- .../Skinning/Legacy/LegacySpinner.cs | 4 ++-- osu.Game/Skinning/LegacyAccuracyCounter.cs | 1 + osu.Game/Skinning/LegacyScoreCounter.cs | 1 + osu.Game/Skinning/LegacySpriteText.cs | 13 ++++++++++++- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 28acb4a996..5a95eac0f1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Origin = Anchor.Centre, Scale = new Vector2(SPRITE_SCALE), Y = SPINNER_TOP_OFFSET + 299, - }.With(s => s.Font = s.Font.With(fixedWidth: false)), + }, spmBackground = new Sprite { Anchor = Anchor.TopCentre, @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Origin = Anchor.TopRight, Scale = new Vector2(SPRITE_SCALE * 0.9f), Position = new Vector2(80, 448 + spm_hide_offset), - }.With(s => s.Font = s.Font.With(fixedWidth: false)), + }, } }); } diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 326257c25f..ed12292eb3 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -25,6 +25,7 @@ namespace osu.Game.Skinning { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, + FixedWidth = true, }; } } diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index d238369be1..a86f122836 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -28,6 +28,7 @@ namespace osu.Game.Skinning { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, + FixedWidth = true, }; } } diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index a803ef8747..041a32e8de 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.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.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; @@ -13,6 +14,7 @@ namespace osu.Game.Skinning public sealed partial class LegacySpriteText : OsuSpriteText { public Vector2? MaxSizePerGlyph { get; init; } + public bool FixedWidth { get; init; } private readonly LegacyFont font; @@ -22,6 +24,15 @@ namespace osu.Game.Skinning protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x' }; + // ReSharper disable once UnusedMember.Global + // being unused is the point here + public new FontUsage Font + { + get => base.Font; + set => throw new InvalidOperationException(@"Attempting to use this setter will not work correctly. " + + $@"Use specific init-only properties exposed by {nameof(LegacySpriteText)} instead."); + } + public LegacySpriteText(LegacyFont font) { this.font = font; @@ -33,7 +44,7 @@ namespace osu.Game.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin) { - Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: true); + base.Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: FixedWidth); Spacing = new Vector2(-skin.GetFontOverlap(font), 0); glyphStore = new LegacyGlyphStore(skin, MaxSizePerGlyph); From 2f9b50172e0b0f261a94585d2c525405a77eaa48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 22:09:33 +0200 Subject: [PATCH 2048/2100] Add failing test coverage for video events affecting storyboard time bounds --- .../Formats/LegacyStoryboardDecoderTest.cs | 21 +++++++++++++++++++ .../video-background-events-ignored.osb | 5 +++++ 2 files changed, 26 insertions(+) create mode 100644 osu.Game.Tests/Resources/video-background-events-ignored.osb diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 34ff8bfd84..647c0aed75 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -287,5 +287,26 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + loop_duration)); } } + + [Test] + public void TestVideoAndBackgroundEventsDoNotAffectStoryboardBounds() + { + var decoder = new LegacyStoryboardDecoder(); + + using var resStream = TestResources.OpenResource("video-background-events-ignored.osb"); + using var stream = new LineBufferedReader(resStream); + + var storyboard = decoder.Decode(stream); + + Assert.Multiple(() => + { + Assert.That(storyboard.GetLayer(@"Video").Elements, Has.Count.EqualTo(1)); + Assert.That(storyboard.GetLayer(@"Video").Elements.Single(), Is.InstanceOf()); + Assert.That(storyboard.GetLayer(@"Video").Elements.Single().StartTime, Is.EqualTo(-5678)); + + Assert.That(storyboard.EarliestEventTime, Is.Null); + Assert.That(storyboard.LatestEventTime, Is.Null); + }); + } } } diff --git a/osu.Game.Tests/Resources/video-background-events-ignored.osb b/osu.Game.Tests/Resources/video-background-events-ignored.osb new file mode 100644 index 0000000000..7525b1fee9 --- /dev/null +++ b/osu.Game.Tests/Resources/video-background-events-ignored.osb @@ -0,0 +1,5 @@ +osu file format v14 + +[Events] +0,-1234,"BG.jpg",0,0 +Video,-5678,"Video.avi",0,0 \ No newline at end of file From 9ce2c1f49c37e63d3d6c18b8fb854ee3b4766adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 22:10:03 +0200 Subject: [PATCH 2049/2100] Exclude video events from being accounted for when calculating storyboard time bounds Closes https://github.com/ppy/osu/issues/25263. In some circumstances, stable allows skipping twice if a particularly long storyboarded intro is being displayed: https://github.com/peppy/osu-stable-reference/blob/master/osu!/GameModes/Play/Player.cs#L1728-L1736 `AllowDoubleSkip` is calculated thus: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/GameModes/Play/Player.cs#L1761-L1770 and `leadInTime` is calculated thus: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/GameModes/Play/Player.cs#L1342-L1351 The key to watch out for here is `{first,last}EventTime`. `EventManager` will calculate it on-the-fly as it adds storyboard elements: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/GameplayElements/Events/EventManager.cs#L253-L256 However, this pathway is only used for sprite, animation, sample, and break events. Video and background events use the following pathway: https://github.com/peppy/osu-stable-reference/blob/master/osu!/GameplayElements/Events/EventManager.cs#L368 Note that this particular overload does not mutate either bound. Which means that for the purposes of determining where a storyboard starts and ends temporally, a video event's start time is essentially ignored. To reflect that, add a clause that excludes video events from calculations of `{Earliest,Latest}EventTime`. --- osu.Game/Storyboards/Storyboard.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 21342831b0..a3137fe1b1 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -30,8 +30,11 @@ namespace osu.Game.Storyboards /// /// /// This iterates all elements and as such should be used sparingly or stored locally. + /// Video and background events are not included to match stable. /// - public double? EarliestEventTime => Layers.SelectMany(l => l.Elements).MinBy(e => e.StartTime)?.StartTime; + public double? EarliestEventTime => Layers.SelectMany(l => l.Elements) + .Where(e => e is not StoryboardVideo) + .MinBy(e => e.StartTime)?.StartTime; /// /// Across all layers, find the latest point in time that a storyboard element ends at. @@ -39,9 +42,12 @@ namespace osu.Game.Storyboards /// /// /// This iterates all elements and as such should be used sparingly or stored locally. - /// Videos and samples return StartTime as their EndTIme. + /// Samples return StartTime as their EndTIme. + /// Video and background events are not included to match stable. /// - public double? LatestEventTime => Layers.SelectMany(l => l.Elements).MaxBy(e => e.GetEndTime())?.GetEndTime(); + public double? LatestEventTime => Layers.SelectMany(l => l.Elements) + .Where(e => e is not StoryboardVideo) + .MaxBy(e => e.GetEndTime())?.GetEndTime(); /// /// Depth of the currently front-most storyboard layer, excluding the overlay layer. From bac306879a4c2965b780c8588f97e95c24e354d0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 03:18:13 +0300 Subject: [PATCH 2050/2100] Minor reword on documentation --- osu.Game/Storyboards/Storyboard.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index a3137fe1b1..8c43b99702 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -30,6 +30,7 @@ namespace osu.Game.Storyboards /// /// /// This iterates all elements and as such should be used sparingly or stored locally. + /// Sample events use their start time as "end time" during this calculation. /// Video and background events are not included to match stable. /// public double? EarliestEventTime => Layers.SelectMany(l => l.Elements) @@ -42,7 +43,7 @@ namespace osu.Game.Storyboards /// /// /// This iterates all elements and as such should be used sparingly or stored locally. - /// Samples return StartTime as their EndTIme. + /// Sample events use their start time as "end time" during this calculation. /// Video and background events are not included to match stable. /// public double? LatestEventTime => Layers.SelectMany(l => l.Elements) From 51b7c97cabd8944cc7dfd840ae4a247d42efeca7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 06:07:49 +0300 Subject: [PATCH 2051/2100] Fix `TestScenePlayerMaxDimensions` bottlenecking CI --- .../Gameplay/TestScenePlayerMaxDimensions.cs | 68 +++---------------- 1 file changed, 10 insertions(+), 58 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs index 68443b234b..741fc7d789 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs @@ -72,66 +72,18 @@ namespace osu.Game.Tests.Visual.Gameplay remove { } } + public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) + { + var texture = base.GetTexture(componentName, wrapModeS, wrapModeT); + + if (texture != null) + texture.ScaleAdjust /= scale_factor; + + return texture; + } + public ISkin FindProvider(Func lookupFunction) => this; public IEnumerable AllSources => new[] { this }; - - protected override IResourceStore CreateTextureLoaderStore(IStorageResourceProvider resources, IResourceStore storage) - => new UpscaledTextureLoaderStore(base.CreateTextureLoaderStore(resources, storage)); - - private class UpscaledTextureLoaderStore : IResourceStore - { - private readonly IResourceStore? textureStore; - - public UpscaledTextureLoaderStore(IResourceStore? textureStore) - { - this.textureStore = textureStore; - } - - public void Dispose() - { - textureStore?.Dispose(); - } - - public TextureUpload Get(string name) - { - var textureUpload = textureStore?.Get(name); - - // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp. - if (textureUpload == null) - return null!; - - return upscale(textureUpload); - } - - public async Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) - { - // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp. - if (textureStore == null) - return null!; - - var textureUpload = await textureStore.GetAsync(name, cancellationToken).ConfigureAwait(false); - - if (textureUpload == null) - return null!; - - return await Task.Run(() => upscale(textureUpload), cancellationToken).ConfigureAwait(false); - } - - private TextureUpload upscale(TextureUpload textureUpload) - { - var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height); - - // The original texture upload will no longer be returned or used. - textureUpload.Dispose(); - - image.Mutate(i => i.Resize(new Size(textureUpload.Width, textureUpload.Height) * scale_factor)); - return new TextureUpload(image); - } - - public Stream? GetStream(string name) => textureStore?.GetStream(name); - - public IEnumerable GetAvailableResources() => textureStore?.GetAvailableResources() ?? Array.Empty(); - } } } } From a53c0adae077847112830952a025df3de4cdfe68 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 06:56:12 +0300 Subject: [PATCH 2052/2100] Remove unused using directive --- .../Visual/Gameplay/TestScenePlayerMaxDimensions.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs index 741fc7d789..6665295e99 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs @@ -3,20 +3,14 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics.Textures; -using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Screens.Play; using osu.Game.Skinning; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; namespace osu.Game.Tests.Visual.Gameplay { From 28e331deed1c27de3f6177b3fe779bf673cc4b97 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 08:29:46 +0300 Subject: [PATCH 2053/2100] Support displaying team seed in `TeamDisplay` --- .../TestSceneDrawableTournamentTeam.cs | 5 +-- .../TournamentTestScene.cs | 3 +- .../Gameplay/Components/TeamDisplay.cs | 34 ++++++++++++++++--- .../Gameplay/Components/TeamScoreDisplay.cs | 5 ++- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs index a809d0747a..8a50cbbe13 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs @@ -21,6 +21,7 @@ namespace osu.Game.Tournament.Tests.Components { FlagName = { Value = "AU" }, FullName = { Value = "Australia" }, + Seed = { Value = "#5" }, Players = { new TournamentUser { Username = "ASecretBox" }, @@ -30,7 +31,7 @@ namespace osu.Game.Tournament.Tests.Components new TournamentUser { Username = "Parkes" }, new TournamentUser { Username = "Shiroha" }, new TournamentUser { Username = "Jordan The Bear" }, - } + }, }; var match = new TournamentMatch { Team1 = { Value = team } }; @@ -100,7 +101,7 @@ namespace osu.Game.Tournament.Tests.Components Cell(i).AddRange(new Drawable[] { new TournamentSpriteText { Text = "TeamDisplay" }, - new TeamDisplay(team, TeamColour.Red, new Bindable(2), 6) + new TeamDisplay(team, TeamColour.Red, new Bindable(2), 6, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tournament.Tests/TournamentTestScene.cs b/osu.Game.Tournament.Tests/TournamentTestScene.cs index f24fc61d85..4106556ee1 100644 --- a/osu.Game.Tournament.Tests/TournamentTestScene.cs +++ b/osu.Game.Tournament.Tests/TournamentTestScene.cs @@ -67,7 +67,7 @@ namespace osu.Game.Tournament.Tests FlagName = { Value = "JP" }, FullName = { Value = "Japan" }, LastYearPlacing = { Value = 10 }, - Seed = { Value = "Low" }, + Seed = { Value = "#12" }, SeedingResults = { new SeedingResult @@ -140,6 +140,7 @@ namespace osu.Game.Tournament.Tests Acronym = { Value = "USA" }, FlagName = { Value = "US" }, FullName = { Value = "United States" }, + Seed = { Value = "#3" }, Players = { new TournamentUser { Username = "Hello" }, diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index 3fdbbb5973..7e63b5b8db 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -14,9 +14,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { private readonly TeamScore score; - private readonly TournamentSpriteTextWithBackground teamText; + private readonly TournamentSpriteTextWithBackground teamNameText; + private readonly TournamentSpriteTextWithBackground teamSeedText; private readonly Bindable teamName = new Bindable("???"); + private readonly Bindable teamSeed = new Bindable(); private bool showScore; @@ -35,7 +37,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components } } - public TeamDisplay(TournamentTeam? team, TeamColour colour, Bindable currentTeamScore, int pointsToWin) + public TeamDisplay(TournamentTeam? team, TeamColour colour, Bindable currentTeamScore, int pointsToWin, bool displaySeed) : base(team) { AutoSizeAxes = Axes.Both; @@ -95,11 +97,29 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components } } }, - teamText = new TournamentSpriteTextWithBackground + new FillFlowContainer { - Scale = new Vector2(0.5f), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), Origin = anchor, Anchor = anchor, + Children = new Drawable[] + { + teamNameText = new TournamentSpriteTextWithBackground + { + Scale = new Vector2(0.5f), + Origin = anchor, + Anchor = anchor, + }, + teamSeedText = new TournamentSpriteTextWithBackground + { + Scale = new Vector2(0.5f), + Origin = anchor, + Anchor = anchor, + Alpha = displaySeed ? 1 : 0, + } + } }, } }, @@ -117,9 +137,13 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components FinishTransforms(true); if (Team != null) + { teamName.BindTo(Team.FullName); + teamSeed.BindTo(Team.Seed); + } - teamName.BindValueChanged(name => teamText.Text.Text = name.NewValue, true); + teamName.BindValueChanged(name => teamNameText.Text.Text = name.NewValue, true); + teamSeed.BindValueChanged(seed => teamSeedText.Text.Text = seed.NewValue, true); } private void updateDisplay() diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs index c7fcfae602..9c2922d030 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs @@ -21,6 +21,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private TeamDisplay? teamDisplay; + public readonly BindableBool DisplaySeed = new BindableBool(); + public bool ShowScore { get => teamDisplay?.ShowScore ?? false; @@ -48,6 +50,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components currentMatch.BindValueChanged(matchChanged); currentTeam.BindValueChanged(teamChanged); + DisplaySeed.BindValueChanged(_ => currentTeam.TriggerChange()); updateMatch(); } @@ -101,7 +104,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components InternalChildren = new Drawable[] { - teamDisplay = new TeamDisplay(team.NewValue, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0), + teamDisplay = new TeamDisplay(team.NewValue, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0, DisplaySeed.Value), }; teamDisplay.ShowScore = wasShowingScores; From e2788a22b153ac5763c9d47a3534f719b4fde497 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 08:30:33 +0300 Subject: [PATCH 2054/2100] Add setting to configure team seed display --- osu.Game.Tournament/Models/LadderInfo.cs | 2 ++ .../Screens/Gameplay/Components/MatchHeader.cs | 5 +++++ .../Screens/Gameplay/GameplayScreen.cs | 12 +++++++++--- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs index 219a2a7bfb..f96dae8044 100644 --- a/osu.Game.Tournament/Models/LadderInfo.cs +++ b/osu.Game.Tournament/Models/LadderInfo.cs @@ -42,5 +42,7 @@ namespace osu.Game.Tournament.Models public Bindable AutoProgressScreens = new BindableBool(true); public Bindable SplitMapPoolByMods = new BindableBool(true); + + public Bindable DisplayTeamSeeds = new BindableBool(); } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index 69f150c8ac..edf1d810ed 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Tournament.Components; @@ -16,6 +17,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private TeamScoreDisplay teamDisplay2 = null!; private DrawableTournamentHeaderLogo logo = null!; + public readonly BindableBool DisplaySeeds = new BindableBool(); + private bool showScores = true; public bool ShowScores @@ -88,11 +91,13 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, + DisplaySeed = { BindTarget = DisplaySeeds }, }, teamDisplay2 = new TeamScoreDisplay(TeamColour.Blue) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, + DisplaySeed = { BindTarget = DisplaySeeds }, }, }; } diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 20188cc5dc..76d7f27421 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tournament.Screens.Gameplay private Drawable chroma = null!; [BackgroundDependencyLoader] - private void load(LadderInfo ladder, MatchIPCInfo ipc) + private void load(MatchIPCInfo ipc) { this.ipc = ipc; @@ -118,12 +118,18 @@ namespace osu.Game.Tournament.Screens.Gameplay LabelText = "Players per team", Current = LadderInfo.PlayersPerTeam, KeyboardStep = 1, - } + }, + new SettingsCheckbox + { + LabelText = "Display team seeds", + Current = LadderInfo.DisplayTeamSeeds, + }, } } }); - ladder.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true); + LadderInfo.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true); + LadderInfo.DisplayTeamSeeds.BindValueChanged(v => header.DisplaySeeds.Value = v.NewValue, true); warmup.BindValueChanged(w => { From 832e30c31a3e68891c23a5f22ad156a3a6224fc4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 08:30:59 +0300 Subject: [PATCH 2055/2100] Adjust horizontal padding in tournament sprite text --- .../Components/TournamentSpriteTextWithBackground.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs index 97cb610021..ce118727cd 100644 --- a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs +++ b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs @@ -29,8 +29,8 @@ namespace osu.Game.Tournament.Components { Colour = TournamentGame.ELEMENT_FOREGROUND_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.SemiBold, size: 50), - Padding = new MarginPadding { Left = 10, Right = 20 }, - Text = text + Padding = new MarginPadding { Horizontal = 10 }, + Text = text, } }; } From 4371a1ab57a5bfb9549d948121bb15f5b1237ecd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 08:42:29 +0300 Subject: [PATCH 2056/2100] Move team seed setting from gameplay screen --- osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs | 9 ++------- osu.Game.Tournament/Screens/Setup/SetupScreen.cs | 6 ++++++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 76d7f27421..3e5ba90c52 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -49,7 +49,8 @@ namespace osu.Game.Tournament.Screens.Gameplay }, header = new MatchHeader { - ShowLogo = false + ShowLogo = false, + DisplaySeeds = { BindTarget = LadderInfo.DisplayTeamSeeds }, }, new Container { @@ -119,17 +120,11 @@ namespace osu.Game.Tournament.Screens.Gameplay Current = LadderInfo.PlayersPerTeam, KeyboardStep = 1, }, - new SettingsCheckbox - { - LabelText = "Display team seeds", - Current = LadderInfo.DisplayTeamSeeds, - }, } } }); LadderInfo.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true); - LadderInfo.DisplayTeamSeeds.BindValueChanged(v => header.DisplaySeeds.Value = v.NewValue, true); warmup.BindValueChanged(w => { diff --git a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs index df1ce69c33..fed9d625ee 100644 --- a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs @@ -140,6 +140,12 @@ namespace osu.Game.Tournament.Screens.Setup Description = "Screens will progress automatically from gameplay -> results -> map pool", Current = LadderInfo.AutoProgressScreens, }, + new LabelledSwitchButton + { + Label = "Display team seeds", + Description = "Team seeds will display alongside each team at the top in gameplay/map pool screens.", + Current = LadderInfo.DisplayTeamSeeds, + }, }; } From 81c1634d4453ad31acceaacd48c5d59a868b73a7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 08:42:40 +0300 Subject: [PATCH 2057/2100] Display team seeds in map pool screen as well --- osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index f80f43bb77..15c6fdb2ae 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -50,6 +50,7 @@ namespace osu.Game.Tournament.Screens.MapPool new MatchHeader { ShowScores = true, + DisplaySeeds = { BindTarget = LadderInfo.DisplayTeamSeeds }, }, mapFlows = new FillFlowContainer> { From 7083c04c5923a9f4494bcdaadfc70de656a33c4d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 09:25:55 +0300 Subject: [PATCH 2058/2100] Refactor logic slightly to display team seed everywhere This change makes the team seed display in "team intro" screen as well. --- .../TestSceneDrawableTournamentTeam.cs | 2 +- .../Components/DrawableTeamSeed.cs | 39 +++++++++++++++++++ .../Components/DrawableTeamTitleWithHeader.cs | 12 +++++- .../Gameplay/Components/MatchHeader.cs | 5 --- .../Gameplay/Components/TeamDisplay.cs | 13 ++----- .../Gameplay/Components/TeamScoreDisplay.cs | 5 +-- .../Screens/Gameplay/GameplayScreen.cs | 1 - .../Screens/MapPool/MapPoolScreen.cs | 1 - 8 files changed, 55 insertions(+), 23 deletions(-) create mode 100644 osu.Game.Tournament/Components/DrawableTeamSeed.cs diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs index 8a50cbbe13..a6eb482d02 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs @@ -101,7 +101,7 @@ namespace osu.Game.Tournament.Tests.Components Cell(i).AddRange(new Drawable[] { new TournamentSpriteText { Text = "TeamDisplay" }, - new TeamDisplay(team, TeamColour.Red, new Bindable(2), 6, true) + new TeamDisplay(team, TeamColour.Red, new Bindable(2), 6) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tournament/Components/DrawableTeamSeed.cs b/osu.Game.Tournament/Components/DrawableTeamSeed.cs new file mode 100644 index 0000000000..a79c63e979 --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTeamSeed.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Tournament.Models; + +namespace osu.Game.Tournament.Components +{ + public partial class DrawableTeamSeed : TournamentSpriteTextWithBackground + { + private readonly TournamentTeam? team; + + private IBindable seed = null!; + private Bindable displaySeed = null!; + + public DrawableTeamSeed(TournamentTeam? team) + { + this.team = team; + } + + [Resolved] + private LadderInfo ladder { get; set; } = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (team == null) + return; + + seed = team.Seed.GetBoundCopy(); + seed.BindValueChanged(s => Text.Text = s.NewValue, true); + + displaySeed = ladder.DisplayTeamSeeds.GetBoundCopy(); + displaySeed.BindValueChanged(v => Alpha = v.NewValue ? 1 : 0, true); + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs index 89f45fc1d3..fc4037d4e1 100644 --- a/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs +++ b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs @@ -22,7 +22,17 @@ namespace osu.Game.Tournament.Components Children = new Drawable[] { new DrawableTeamHeader(colour), - new DrawableTeamTitle(team), + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new DrawableTeamTitle(team), + new DrawableTeamSeed(team), + } + } } }; } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index edf1d810ed..69f150c8ac 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Tournament.Components; @@ -17,8 +16,6 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private TeamScoreDisplay teamDisplay2 = null!; private DrawableTournamentHeaderLogo logo = null!; - public readonly BindableBool DisplaySeeds = new BindableBool(); - private bool showScores = true; public bool ShowScores @@ -91,13 +88,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - DisplaySeed = { BindTarget = DisplaySeeds }, }, teamDisplay2 = new TeamScoreDisplay(TeamColour.Blue) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - DisplaySeed = { BindTarget = DisplaySeeds }, }, }; } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index 7e63b5b8db..49fbc64397 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -15,10 +15,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private readonly TeamScore score; private readonly TournamentSpriteTextWithBackground teamNameText; - private readonly TournamentSpriteTextWithBackground teamSeedText; private readonly Bindable teamName = new Bindable("???"); - private readonly Bindable teamSeed = new Bindable(); private bool showScore; @@ -37,7 +35,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components } } - public TeamDisplay(TournamentTeam? team, TeamColour colour, Bindable currentTeamScore, int pointsToWin, bool displaySeed) + public TeamDisplay(TournamentTeam? team, TeamColour colour, Bindable currentTeamScore, int pointsToWin) : base(team) { AutoSizeAxes = Axes.Both; @@ -112,13 +110,12 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components Origin = anchor, Anchor = anchor, }, - teamSeedText = new TournamentSpriteTextWithBackground + new DrawableTeamSeed(Team) { Scale = new Vector2(0.5f), Origin = anchor, Anchor = anchor, - Alpha = displaySeed ? 1 : 0, - } + }, } }, } @@ -137,13 +134,9 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components FinishTransforms(true); if (Team != null) - { teamName.BindTo(Team.FullName); - teamSeed.BindTo(Team.Seed); - } teamName.BindValueChanged(name => teamNameText.Text.Text = name.NewValue, true); - teamSeed.BindValueChanged(seed => teamSeedText.Text.Text = seed.NewValue, true); } private void updateDisplay() diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs index 9c2922d030..c7fcfae602 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs @@ -21,8 +21,6 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private TeamDisplay? teamDisplay; - public readonly BindableBool DisplaySeed = new BindableBool(); - public bool ShowScore { get => teamDisplay?.ShowScore ?? false; @@ -50,7 +48,6 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components currentMatch.BindValueChanged(matchChanged); currentTeam.BindValueChanged(teamChanged); - DisplaySeed.BindValueChanged(_ => currentTeam.TriggerChange()); updateMatch(); } @@ -104,7 +101,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components InternalChildren = new Drawable[] { - teamDisplay = new TeamDisplay(team.NewValue, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0, DisplaySeed.Value), + teamDisplay = new TeamDisplay(team.NewValue, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0), }; teamDisplay.ShowScore = wasShowingScores; diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 3e5ba90c52..b2152eaf3d 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -50,7 +50,6 @@ namespace osu.Game.Tournament.Screens.Gameplay header = new MatchHeader { ShowLogo = false, - DisplaySeeds = { BindTarget = LadderInfo.DisplayTeamSeeds }, }, new Container { diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index 15c6fdb2ae..f80f43bb77 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -50,7 +50,6 @@ namespace osu.Game.Tournament.Screens.MapPool new MatchHeader { ShowScores = true, - DisplaySeeds = { BindTarget = LadderInfo.DisplayTeamSeeds }, }, mapFlows = new FillFlowContainer> { From e76a5f9419276d8eaa8bea22c899fd1aa651c80b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 10:18:15 +0300 Subject: [PATCH 2059/2100] Fix failing tests --- .../Components/TestSceneDrawableTournamentTeam.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs index a6eb482d02..43adcc61bf 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Tests.Visual; @@ -14,9 +15,14 @@ namespace osu.Game.Tournament.Tests.Components { public partial class TestSceneDrawableTournamentTeam : OsuGridTestScene { + [Cached] + protected LadderInfo Ladder { get; private set; } = new LadderInfo(); + public TestSceneDrawableTournamentTeam() : base(4, 3) { + AddToggleStep("toggle seed view", v => Ladder.DisplayTeamSeeds.Value = v); + var team = new TournamentTeam { FlagName = { Value = "AU" }, From cfc0520481df18553b7252f1f63149ee89e4c9e9 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sat, 28 Oct 2023 12:13:13 +0200 Subject: [PATCH 2060/2100] Add failing test --- .../SongSelect/TestScenePlaySongSelect.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 6737ec9739..7313bde8fe 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -13,6 +13,7 @@ using osu.Framework.Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; @@ -1111,6 +1112,23 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("0 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "0 matches"); } + [Test] + public void TestCutInFilterTextBox() + { + createSongSelect(); + + AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType().First().Text = "nonono"); + AddStep("select all", () => InputManager.Keys(PlatformAction.SelectAll)); + AddStep("press ctrl-x", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.X); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + AddAssert("filter text cleared", () => songSelect!.FilterControl.ChildrenOfType().First().Text, () => Is.Empty); + } + private void waitForInitialSelection() { AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault); From 366e41f11182c1220b2c8e33bc74172fa7543986 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sat, 28 Oct 2023 12:23:23 +0200 Subject: [PATCH 2061/2100] Use local workaround instead of disabling clipboard entirely --- osu.Game/Screens/Select/FilterControl.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index b7dc18e46a..c15bd76ef8 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Collections; @@ -23,6 +24,7 @@ using osu.Game.Rulesets; using osu.Game.Screens.Select.Filter; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Screens.Select { @@ -254,9 +256,6 @@ namespace osu.Game.Screens.Select public OsuSpriteText FilterText { get; private set; } - // clipboard is disabled because one of the "cut" platform key bindings (shift-delete) conflicts with the beatmap deletion action. - protected override bool AllowClipboardExport => false; - public FilterControlTextBox() { Height += filter_text_size; @@ -277,6 +276,15 @@ namespace osu.Game.Screens.Select Colour = colours.Yellow }); } + + public override bool OnPressed(KeyBindingPressEvent e) + { + // the "cut" platform key binding (shift-delete) conflicts with the beatmap deletion action. + if (e.Action == PlatformAction.Cut && e.ShiftPressed && e.CurrentState.Keyboard.Keys.IsPressed(Key.Delete)) + return false; + + return base.OnPressed(e); + } } } } From c38c8e933ab0aba67d7a36ccff1413cde465dd8e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 16:52:33 +0300 Subject: [PATCH 2062/2100] Change tournament date text box parsing to use invariant culture info --- osu.Game.Tournament/Components/DateTextBox.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs index ab643a5cb5..dd70d5856d 100644 --- a/osu.Game.Tournament/Components/DateTextBox.cs +++ b/osu.Game.Tournament/Components/DateTextBox.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Globalization; using osu.Framework.Bindables; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; @@ -23,13 +24,13 @@ namespace osu.Game.Tournament.Components base.Current = new Bindable(string.Empty); current.BindValueChanged(dto => - base.Current.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"), true); + base.Current.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", DateTimeFormatInfo.InvariantInfo), true); ((OsuTextBox)Control).OnCommit += (sender, _) => { try { - current.Value = DateTimeOffset.Parse(sender.Text); + current.Value = DateTimeOffset.Parse(sender.Text, DateTimeFormatInfo.InvariantInfo); } catch { From d877536dc0ec8d1f8f1889be73740a58ef29f869 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 29 Oct 2023 01:03:38 +0300 Subject: [PATCH 2063/2100] Select all text content in `SearchTextBox` on focus --- osu.Game/Graphics/UserInterface/SearchTextBox.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/SearchTextBox.cs b/osu.Game/Graphics/UserInterface/SearchTextBox.cs index a2e0ab6482..b554c2bbd8 100644 --- a/osu.Game/Graphics/UserInterface/SearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SearchTextBox.cs @@ -18,6 +18,12 @@ namespace osu.Game.Graphics.UserInterface PlaceholderText = HomeStrings.SearchPlaceholder; } + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + SelectAll(); + } + public override bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) From 31c6973bb646272b3ddcb2b3405d1802c8bf770c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 29 Oct 2023 01:03:45 +0300 Subject: [PATCH 2064/2100] Add test coverage --- .../UserInterface/TestSceneSearchTextBox.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneSearchTextBox.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSearchTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSearchTextBox.cs new file mode 100644 index 0000000000..153525d24a --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSearchTextBox.cs @@ -0,0 +1,38 @@ +// 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.Graphics; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneSearchTextBox : OsuTestScene + { + private SearchTextBox textBox = null!; + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = textBox = new SearchTextBox + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 400, + Scale = new Vector2(2f), + HoldFocus = true, + }; + }); + + [Test] + public void TestSelectionOnFocus() + { + AddStep("set text", () => textBox.Text = "some text"); + AddAssert("no text selected", () => textBox.SelectedText == string.Empty); + AddStep("hide text box", () => textBox.Hide()); + AddStep("show text box", () => textBox.Show()); + AddAssert("search text selected", () => textBox.SelectedText == textBox.Text); + } + } +} From ec9ae12bbd880f2f9a1c31adca3d09e516d0dbb4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 29 Oct 2023 01:43:49 +0300 Subject: [PATCH 2065/2100] Update API response model to accept array of tournament banners --- .../Online/TestSceneUserProfileOverlay.cs | 27 +++++++++++++++---- .../Online/API/Requests/Responses/APIUser.cs | 5 ++-- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index b57b0b7312..a321a194a9 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -121,12 +121,29 @@ namespace osu.Game.Tests.Visual.Online Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray() }, }, - TournamentBanner = new TournamentBanner + TournamentBanners = new[] { - Id = 13926, - TournamentId = 35, - ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2022/profile/winner_US.jpg", - Image = "https://assets.ppy.sh/tournament-banners/official/owc2022/profile/winner_US@2x.jpg", + new TournamentBanner + { + Id = 15588, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN@2x.jpg" + }, + new TournamentBanner + { + Id = 15589, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH@2x.jpg" + }, + new TournamentBanner + { + Id = 15590, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL@2x.jpg" + } }, Badges = new[] { diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index d9208d0662..7c4093006d 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -234,9 +234,8 @@ namespace osu.Game.Online.API.Requests.Responses set => Statistics.RankHistory = value; } - [JsonProperty(@"active_tournament_banner")] - [CanBeNull] - public TournamentBanner TournamentBanner; + [JsonProperty(@"active_tournament_banners")] + public TournamentBanner[] TournamentBanners; [JsonProperty("badges")] public Badge[] Badges; From 922ad80cfc9faf6fd63a0a5f01a266247924c4f8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 29 Oct 2023 01:44:21 +0300 Subject: [PATCH 2066/2100] Update user profile overlay to show more than one tournament banner --- .../Profile/Header/BannerHeaderContainer.cs | 15 ++++++++------- .../Header/Components/DrawableTournamentBanner.cs | 9 ++++++++- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs index 8e6648dc4b..7ed58200ec 100644 --- a/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.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.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -11,7 +12,7 @@ using osu.Game.Overlays.Profile.Header.Components; namespace osu.Game.Overlays.Profile.Header { - public partial class BannerHeaderContainer : CompositeDrawable + public partial class BannerHeaderContainer : FillFlowContainer { public readonly Bindable User = new Bindable(); @@ -19,9 +20,9 @@ namespace osu.Game.Overlays.Profile.Header private void load() { Alpha = 0; - RelativeSizeAxes = Axes.Both; - FillMode = FillMode.Fit; - FillAspectRatio = 1000 / 60f; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; } protected override void LoadComplete() @@ -40,13 +41,13 @@ namespace osu.Game.Overlays.Profile.Header ClearInternal(); - var banner = user?.TournamentBanner; + var banners = user?.TournamentBanners; - if (banner != null) + if (banners?.Length > 0) { Show(); - LoadComponentAsync(new DrawableTournamentBanner(banner), AddInternal, cancellationTokenSource.Token); + LoadComponentsAsync(banners.Select(b => new DrawableTournamentBanner(b)), AddRangeInternal, cancellationTokenSource.Token); } else { diff --git a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs index 26d333ff95..c099009ca4 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs @@ -15,12 +15,13 @@ namespace osu.Game.Overlays.Profile.Header.Components [LongRunningLoad] public partial class DrawableTournamentBanner : OsuClickableContainer { + private const float banner_aspect_ratio = 60 / 1000f; private readonly TournamentBanner banner; public DrawableTournamentBanner(TournamentBanner banner) { this.banner = banner; - RelativeSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.X; } [BackgroundDependencyLoader] @@ -41,6 +42,12 @@ namespace osu.Game.Overlays.Profile.Header.Components this.FadeInFromZero(200); } + protected override void Update() + { + base.Update(); + Height = DrawWidth * banner_aspect_ratio; + } + public override LocalisableString TooltipText => "view in browser"; } } From 204ebfade7ecaf5a426bfc21a6b89cfa6393fed8 Mon Sep 17 00:00:00 2001 From: Rowe Wilson Frederisk Holme Date: Sun, 29 Oct 2023 21:25:15 +0800 Subject: [PATCH 2067/2100] Small update to README of Templates --- Templates/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Templates/README.md b/Templates/README.md index cf25a89273..28aaee3290 100644 --- a/Templates/README.md +++ b/Templates/README.md @@ -7,7 +7,7 @@ Templates for use when creating osu! dependent projects. Create a fully-testable ```bash # install (or update) templates package. # this only needs to be done once -dotnet new -i ppy.osu.Game.Templates +dotnet new install ppy.osu.Game.Templates # create an empty freeform ruleset dotnet new ruleset -n MyCoolRuleset From af10dbb76c4b2ab6217655f7b0eeee124acc3635 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 30 Oct 2023 06:19:52 +0300 Subject: [PATCH 2068/2100] Add test case with many tournament banners --- .../Online/TestSceneUserProfileHeader.cs | 266 +++++++++++++++++- 1 file changed, 265 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs index 4f28baa849..c9e5a3315c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs @@ -5,8 +5,10 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Configuration; +using osu.Game.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Profile; @@ -28,7 +30,14 @@ namespace osu.Game.Tests.Visual.Online [SetUpSteps] public void SetUpSteps() { - AddStep("create header", () => Child = header = new ProfileHeader()); + AddStep("create header", () => + { + Child = new OsuScrollContainer(Direction.Vertical) + { + RelativeSizeAxes = Axes.Both, + Child = header = new ProfileHeader() + }; + }); } [Test] @@ -136,5 +145,260 @@ namespace osu.Game.Tests.Visual.Online PreviousUsernames = new[] { "tsrk.", "quoicoubeh", "apagnan", "epita" } }, new OsuRuleset().RulesetInfo)); } + + [Test] + public void TestManyTournamentBanners() + { + AddStep("Show user w/ many tournament banners", () => header.User.Value = new UserProfileData(new APIUser + { + Id = 728, + Username = "Certain Guy", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", + Statistics = new UserStatistics + { + IsRanked = false, + // web will sometimes return non-empty rank history even for unranked users. + RankHistory = new APIRankHistory + { + Mode = @"osu", + Data = Enumerable.Range(2345, 85).ToArray() + }, + }, + TournamentBanners = new[] + { + new TournamentBanner + { + Id = 15329, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK@2x.jpg" + }, + new TournamentBanner + { + Id = 15588, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN@2x.jpg" + }, + new TournamentBanner + { + Id = 15589, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH@2x.jpg" + }, + new TournamentBanner + { + Id = 15590, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL@2x.jpg" + }, + new TournamentBanner + { + Id = 15591, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_JP.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_JP@2x.jpg" + }, + new TournamentBanner + { + Id = 15592, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RU.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RU@2x.jpg" + }, + new TournamentBanner + { + Id = 15593, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KR.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KR@2x.jpg" + }, + new TournamentBanner + { + Id = 15594, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NZ.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NZ@2x.jpg" + }, + new TournamentBanner + { + Id = 15595, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TH.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TH@2x.jpg" + }, + new TournamentBanner + { + Id = 15596, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TW.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TW@2x.jpg" + }, + new TournamentBanner + { + Id = 15603, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ID.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ID@2x.jpg" + }, + new TournamentBanner + { + Id = 15604, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KZ.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KZ@2x.jpg" + }, + new TournamentBanner + { + Id = 15605, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AR.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AR@2x.jpg" + }, + new TournamentBanner + { + Id = 15606, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_BR.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_BR@2x.jpg" + }, + new TournamentBanner + { + Id = 15607, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL@2x.jpg" + }, + new TournamentBanner + { + Id = 15639, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_MX.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_MX@2x.jpg" + }, + new TournamentBanner + { + Id = 15640, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AU.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AU@2x.jpg" + }, + new TournamentBanner + { + Id = 15641, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_IT.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_IT@2x.jpg" + }, + new TournamentBanner + { + Id = 15642, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_UA.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_UA@2x.jpg" + }, + new TournamentBanner + { + Id = 15643, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NL.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NL@2x.jpg" + }, + new TournamentBanner + { + Id = 15644, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FI.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FI@2x.jpg" + }, + new TournamentBanner + { + Id = 15645, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RO.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RO@2x.jpg" + }, + new TournamentBanner + { + Id = 15646, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SG.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SG@2x.jpg" + }, + new TournamentBanner + { + Id = 15647, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_DE.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_DE@2x.jpg" + }, + new TournamentBanner + { + Id = 15648, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ES.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ES@2x.jpg" + }, + new TournamentBanner + { + Id = 15649, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SE.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SE@2x.jpg" + }, + new TournamentBanner + { + Id = 15650, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CA.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CA@2x.jpg" + }, + new TournamentBanner + { + Id = 15651, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NO.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NO@2x.jpg" + }, + new TournamentBanner + { + Id = 15652, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_GB.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_GB@2x.jpg" + }, + new TournamentBanner + { + Id = 15653, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_US.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_US@2x.jpg" + }, + new TournamentBanner + { + Id = 15654, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL@2x.jpg" + }, + new TournamentBanner + { + Id = 15655, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FR.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FR@2x.jpg" + }, + new TournamentBanner + { + Id = 15686, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK@2x.jpg" + } + } + }, new OsuRuleset().RulesetInfo)); + } } } From 984c30ded662bc6091c980dc6d6fcf1da880479e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 30 Oct 2023 06:20:13 +0300 Subject: [PATCH 2069/2100] Load each tournament banner as soon as it is loaded --- .../Overlays/Profile/Header/BannerHeaderContainer.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs index 7ed58200ec..424ab4a529 100644 --- a/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs @@ -1,7 +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.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -47,7 +46,15 @@ namespace osu.Game.Overlays.Profile.Header { Show(); - LoadComponentsAsync(banners.Select(b => new DrawableTournamentBanner(b)), AddRangeInternal, cancellationTokenSource.Token); + for (int index = 0; index < banners.Length; index++) + { + int displayIndex = index; + LoadComponentAsync(new DrawableTournamentBanner(banners[index]), asyncBanner => + { + // load in stable order regardless of async load order. + Insert(displayIndex, asyncBanner); + }, cancellationTokenSource.Token); + } } else { From c7bc8e686543e4fe6e823d06b5d79be144d8ad6e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 30 Oct 2023 06:41:01 +0300 Subject: [PATCH 2070/2100] Move behaviour to settings search text box only --- .../Visual/Settings/TestSceneSettingsPanel.cs | 11 +++++++++++ .../Graphics/UserInterface/SearchTextBox.cs | 6 ------ osu.Game/Overlays/SettingsPanel.cs | 2 +- osu.Game/Overlays/SettingsSearchTextBox.cs | 18 ++++++++++++++++++ 4 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Overlays/SettingsSearchTextBox.cs diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs index 24c2eee783..69e489b247 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs @@ -140,6 +140,17 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType().FirstOrDefault()?.HasFocus == true); } + [Test] + public void TestSearchTextBoxSelectedOnShow() + { + SearchTextBox searchTextBox = null!; + + AddStep("set text", () => (searchTextBox = settings.SectionsContainer.ChildrenOfType().First()).Current.Value = "some text"); + AddAssert("no text selected", () => searchTextBox.SelectedText == string.Empty); + AddRepeatStep("toggle visibility", () => settings.ToggleVisibility(), 2); + AddAssert("search text selected", () => searchTextBox.SelectedText == searchTextBox.Current.Value); + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Graphics/UserInterface/SearchTextBox.cs b/osu.Game/Graphics/UserInterface/SearchTextBox.cs index b554c2bbd8..a2e0ab6482 100644 --- a/osu.Game/Graphics/UserInterface/SearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SearchTextBox.cs @@ -18,12 +18,6 @@ namespace osu.Game.Graphics.UserInterface PlaceholderText = HomeStrings.SearchPlaceholder; } - protected override void OnFocus(FocusEvent e) - { - base.OnFocus(e); - SelectAll(); - } - public override bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 2517a58491..3bac6c400f 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -135,7 +135,7 @@ namespace osu.Game.Overlays }, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Child = searchTextBox = new SeekLimitedSearchTextBox + Child = searchTextBox = new SettingsSearchTextBox { RelativeSizeAxes = Axes.X, Origin = Anchor.TopCentre, diff --git a/osu.Game/Overlays/SettingsSearchTextBox.cs b/osu.Game/Overlays/SettingsSearchTextBox.cs new file mode 100644 index 0000000000..bafa6e26eb --- /dev/null +++ b/osu.Game/Overlays/SettingsSearchTextBox.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable +using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays +{ + public partial class SettingsSearchTextBox : SeekLimitedSearchTextBox + { + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + SelectAll(); + } + } +} From d90f29a5ff4a4618955b7ac7c4e743d39b4aa03a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Oct 2023 15:07:26 +0900 Subject: [PATCH 2071/2100] Improve log output surrounding score submission --- osu.Game/Screens/Play/SubmittingPlayer.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 5fa6508a31..a75546f835 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -188,7 +188,10 @@ namespace osu.Game.Screens.Play { // token may be null if the request failed but gameplay was still allowed (see HandleTokenRetrievalFailure). if (token == null) + { + Logger.Log("No token, skipping score submission"); return Task.CompletedTask; + } if (scoreSubmissionSource != null) return scoreSubmissionSource.Task; @@ -197,6 +200,8 @@ namespace osu.Game.Screens.Play if (!score.ScoreInfo.Statistics.Any(s => s.Key.IsHit() && s.Value > 0)) return Task.CompletedTask; + Logger.Log($"Beginning score submission (token:{token.Value})..."); + scoreSubmissionSource = new TaskCompletionSource(); var request = CreateSubmissionRequest(score, token.Value); @@ -206,11 +211,12 @@ namespace osu.Game.Screens.Play score.ScoreInfo.Position = s.Position; scoreSubmissionSource.SetResult(true); + Logger.Log($"Score submission completed! (token:{token.Value} id:{s.ID})"); }; request.Failure += e => { - Logger.Error(e, $"Failed to submit score ({e.Message})"); + Logger.Error(e, $"Failed to submit score (token:{token.Value}): {e.Message}"); scoreSubmissionSource.SetResult(false); }; From a8c3f598457ee9079ba97d2f381f75f7774890dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Oct 2023 15:07:35 +0900 Subject: [PATCH 2072/2100] Clean up type display for web requests in logs --- osu.Game/Online/API/APIRequest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index cd6e8df754..6b6b222043 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -7,6 +7,7 @@ using System; using System.Globalization; using JetBrains.Annotations; using Newtonsoft.Json; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.IO.Network; using osu.Framework.Logging; using osu.Game.Extensions; @@ -46,7 +47,7 @@ namespace osu.Game.Online.API if (WebRequest != null) { Response = ((OsuJsonWebRequest)WebRequest).ResponseObject; - Logger.Log($"{GetType()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network); + Logger.Log($"{GetType().ReadableName()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network); } } From a91b704d21df8adeca8068dd448d2ac9424010c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Oct 2023 15:10:10 +0900 Subject: [PATCH 2073/2100] Fix some new nullable inspections --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 8 ++------ osu.Game/Rulesets/Edit/SelectionBlueprint.cs | 2 ++ osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs | 2 ++ osu.Game/Screens/Play/SquareGraph.cs | 2 ++ 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index c62659d67a..3cb9b96090 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -49,13 +49,9 @@ namespace osu.Game.Rulesets.Osu.Objects set { path.ControlPoints.Clear(); - path.ExpectedDistance.Value = null; + path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position, c.Type))); - if (value != null) - { - path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position, c.Type))); - path.ExpectedDistance.Value = value.ExpectedDistance.Value; - } + path.ExpectedDistance.Value = value.ExpectedDistance.Value; } } diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs index 158c3c102c..3ed7558bcb 100644 --- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs +++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs @@ -5,6 +5,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -51,6 +52,7 @@ namespace osu.Game.Rulesets.Edit private SelectionState state; + [CanBeNull] public event Action StateChanged; public SelectionState State diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs index 1506b884b4..85ea881006 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs @@ -166,6 +166,8 @@ namespace osu.Game.Screens.Backgrounds public override void Add(Drawable drawable) { + ArgumentNullException.ThrowIfNull(drawable); + if (drawable is Background) throw new InvalidOperationException($"Use {nameof(Background)} to set a background."); diff --git a/osu.Game/Screens/Play/SquareGraph.cs b/osu.Game/Screens/Play/SquareGraph.cs index b53e86a41b..0c7b485755 100644 --- a/osu.Game/Screens/Play/SquareGraph.cs +++ b/osu.Game/Screens/Play/SquareGraph.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using JetBrains.Annotations; using osu.Framework; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -190,6 +191,7 @@ namespace osu.Game.Screens.Play private const float padding = 2; public const float WIDTH = cube_size + padding; + [CanBeNull] public event Action StateChanged; private readonly List drawableRows = new List(); From 96dd7b3333014cb5e8e404d129b163de1f72f6e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Oct 2023 15:44:16 +0900 Subject: [PATCH 2074/2100] Update the last played date of a beatmap when importing a replay by the local user --- osu.Game.Tests/ImportTest.cs | 4 ++ osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 59 +++++++++++++++++++++ osu.Game/Online/API/DummyAPIAccess.cs | 2 +- osu.Game/Scoring/ScoreImporter.cs | 3 ++ 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/ImportTest.cs b/osu.Game.Tests/ImportTest.cs index 3f0f8a4f14..27b8d3f21e 100644 --- a/osu.Game.Tests/ImportTest.cs +++ b/osu.Game.Tests/ImportTest.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Game.Database; +using osu.Game.Online.API; using osu.Game.Tests.Resources; namespace osu.Game.Tests @@ -46,12 +47,15 @@ namespace osu.Game.Tests public partial class TestOsuGameBase : OsuGameBase { public RealmAccess Realm => Dependencies.Get(); + public new IAPIProvider API => base.API; private readonly bool withBeatmap; public TestOsuGameBase(bool withBeatmap) { this.withBeatmap = withBeatmap; + + base.API = new DummyAPIAccess(); } [BackgroundDependencyLoader] diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 892ceea185..d1bacaaf69 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -12,6 +12,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Game.IO.Archives; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -67,6 +68,64 @@ namespace osu.Game.Tests.Scores.IO } } + [TestCase(false)] + [TestCase(true)] + public void TestLastPlayedUpdate(bool isLocalUser) + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = LoadOsuIntoHost(host, true); + + if (!isLocalUser) + osu.API.Logout(); + + var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); + var beatmapInfo = beatmap.Beatmaps.First(); + + DateTimeOffset replayDate = DateTimeOffset.Now; + + var toImport = new ScoreInfo + { + Rank = ScoreRank.B, + TotalScore = 987654, + Accuracy = 0.8, + MaxCombo = 500, + Combo = 250, + User = new APIUser + { + Username = "Test user", + Id = DummyAPIAccess.DUMMY_USER_ID, + }, + Date = replayDate, + OnlineID = 12345, + Ruleset = new OsuRuleset().RulesetInfo, + BeatmapInfo = beatmapInfo + }; + + var imported = LoadScoreIntoOsu(osu, toImport); + + Assert.AreEqual(toImport.Rank, imported.Rank); + Assert.AreEqual(toImport.TotalScore, imported.TotalScore); + Assert.AreEqual(toImport.Accuracy, imported.Accuracy); + Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo); + Assert.AreEqual(toImport.User.Username, imported.User.Username); + Assert.AreEqual(toImport.Date, imported.Date); + Assert.AreEqual(toImport.OnlineID, imported.OnlineID); + + if (isLocalUser) + Assert.That(imported.BeatmapInfo!.LastPlayed, Is.EqualTo(replayDate)); + else + Assert.That(imported.BeatmapInfo!.LastPlayed, Is.Null); + } + finally + { + host.Exit(); + } + } + } + [Test] public void TestImportMods() { diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 2764247f5c..d585124db6 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -112,7 +112,7 @@ namespace osu.Game.Online.API LocalUser.Value = new APIUser { Username = username, - Id = 1001, + Id = DUMMY_USER_ID, }; state.Value = APIState.Online; diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 7473d887c3..32a528f218 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -87,6 +87,9 @@ namespace osu.Game.Scoring if (!model.Ruleset.IsManaged) model.Ruleset = realm.Find(model.Ruleset.ShortName)!; + if (api.IsLoggedIn && api.LocalUser.Value.OnlineID == model.UserID && (model.BeatmapInfo.LastPlayed == null || model.Date > model.BeatmapInfo.LastPlayed)) + model.BeatmapInfo.LastPlayed = model.Date; + // These properties are known to be non-null, but these final checks ensure a null hasn't come from somewhere (or the refetch has failed). // Under no circumstance do we want these to be written to realm as null. ArgumentNullException.ThrowIfNull(model.BeatmapInfo); From c1c8e2968f1f620851c4e608043051f07b944d8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Oct 2023 15:46:09 +0900 Subject: [PATCH 2075/2100] Move operation to after user population --- osu.Game/Scoring/ScoreImporter.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 32a528f218..b216c0897e 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -87,9 +87,6 @@ namespace osu.Game.Scoring if (!model.Ruleset.IsManaged) model.Ruleset = realm.Find(model.Ruleset.ShortName)!; - if (api.IsLoggedIn && api.LocalUser.Value.OnlineID == model.UserID && (model.BeatmapInfo.LastPlayed == null || model.Date > model.BeatmapInfo.LastPlayed)) - model.BeatmapInfo.LastPlayed = model.Date; - // These properties are known to be non-null, but these final checks ensure a null hasn't come from somewhere (or the refetch has failed). // Under no circumstance do we want these to be written to realm as null. ArgumentNullException.ThrowIfNull(model.BeatmapInfo); @@ -185,6 +182,12 @@ namespace osu.Game.Scoring base.PostImport(model, realm, parameters); populateUserDetails(model); + + Debug.Assert(model.BeatmapInfo != null); + + // This needs to be run after user detail population to ensure we have a valid user id. + if (api.IsLoggedIn && api.LocalUser.Value.OnlineID == model.UserID && (model.BeatmapInfo.LastPlayed == null || model.Date > model.BeatmapInfo.LastPlayed)) + model.BeatmapInfo.LastPlayed = model.Date; } /// From 0dfb41b7966de7823982f3f18b2a5adfa38ac1c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 09:28:37 +0100 Subject: [PATCH 2076/2100] Add test coverage for not updating `LastPlayed` due to newer plays --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 54 +++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index d1bacaaf69..dd724d268e 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -11,6 +11,8 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -126,6 +128,58 @@ namespace osu.Game.Tests.Scores.IO } } + [Test] + public void TestLastPlayedNotUpdatedDueToNewerPlays() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = LoadOsuIntoHost(host, true); + + var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); + var beatmapInfo = beatmap.Beatmaps.First(); + + var realmAccess = osu.Dependencies.Get(); + realmAccess.Write(r => r.Find(beatmapInfo.ID)!.LastPlayed = new DateTimeOffset(2023, 10, 30, 0, 0, 0, TimeSpan.Zero)); + + var toImport = new ScoreInfo + { + Rank = ScoreRank.B, + TotalScore = 987654, + Accuracy = 0.8, + MaxCombo = 500, + Combo = 250, + User = new APIUser + { + Username = "Test user", + Id = DummyAPIAccess.DUMMY_USER_ID, + }, + Date = new DateTimeOffset(2023, 10, 27, 0, 0, 0, TimeSpan.Zero), + OnlineID = 12345, + Ruleset = new OsuRuleset().RulesetInfo, + BeatmapInfo = beatmapInfo + }; + + var imported = LoadScoreIntoOsu(osu, toImport); + + Assert.AreEqual(toImport.Rank, imported.Rank); + Assert.AreEqual(toImport.TotalScore, imported.TotalScore); + Assert.AreEqual(toImport.Accuracy, imported.Accuracy); + Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo); + Assert.AreEqual(toImport.User.Username, imported.User.Username); + Assert.AreEqual(toImport.Date, imported.Date); + Assert.AreEqual(toImport.OnlineID, imported.OnlineID); + + Assert.That(imported.BeatmapInfo!.LastPlayed, Is.EqualTo(new DateTimeOffset(2023, 10, 30, 0, 0, 0, TimeSpan.Zero))); + } + finally + { + host.Exit(); + } + } + } + [Test] public void TestImportMods() { From 39abb8e4085d17bc8ff64a80f19e268a3d4a5b7f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 30 Oct 2023 11:54:19 +0300 Subject: [PATCH 2077/2100] Only run "select all on focus" behaviour on desktop platforms --- osu.Game/Overlays/SettingsSearchTextBox.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SettingsSearchTextBox.cs b/osu.Game/Overlays/SettingsSearchTextBox.cs index bafa6e26eb..84cff1b508 100644 --- a/osu.Game/Overlays/SettingsSearchTextBox.cs +++ b/osu.Game/Overlays/SettingsSearchTextBox.cs @@ -12,7 +12,11 @@ namespace osu.Game.Overlays protected override void OnFocus(FocusEvent e) { base.OnFocus(e); - SelectAll(); + + // on mobile platforms, focus is not held by the search text box, and the select all feature + // will not make sense on it, and might annoy the user when they try to focus manually. + if (HoldFocus) + SelectAll(); } } } From 6be02966b9e79dfbb94ad372b932decc78ee65f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Oct 2023 18:06:09 +0900 Subject: [PATCH 2078/2100] Add test coverage of failing context menu display --- .../Editing/TestSceneTimelineSelection.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs index 50eeb9a54b..0051488029 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs @@ -7,8 +7,10 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; @@ -44,6 +46,47 @@ namespace osu.Game.Tests.Visual.Editing }); } + [Test] + public void TestContextMenuWithObjectBehind() + { + TimelineHitObjectBlueprint blueprint; + + AddStep("add object", () => + { + EditorBeatmap.Add(new HitCircle { StartTime = 3000 }); + }); + + AddStep("enter slider placement", () => + { + InputManager.Key(Key.Number3); + InputManager.MoveMouseTo(ScreenSpaceDrawQuad.Centre); + }); + + AddStep("start conflicting slider", () => + { + InputManager.Click(MouseButton.Left); + + blueprint = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(blueprint.ScreenSpaceDrawQuad.TopLeft - new Vector2(10, 0)); + }); + + AddStep("end conflicting slider", () => + { + InputManager.Click(MouseButton.Right); + }); + + AddStep("click object", () => + { + InputManager.Key(Key.Number1); + blueprint = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(blueprint); + InputManager.Click(MouseButton.Left); + }); + + AddStep("right click", () => InputManager.Click(MouseButton.Right)); + AddAssert("context menu open", () => this.ChildrenOfType().SingleOrDefault()?.State == MenuState.Open); + } + [Test] public void TestNudgeSelection() { From 57d88a0ac459398a14aafd8923238ce1bc012e7f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Oct 2023 17:46:04 +0900 Subject: [PATCH 2079/2100] Fix right clicks on timeline objects potentially getting eaten by playfield area `SelectionHandler` is receiving input from anywhere out of necessity: https://github.com/ppy/osu/blob/19f892687a0607afbe4e0d010366dc2a66236073/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs#L119-L125 Also important is that `BlueprintContainer` will selectively not block right clicks to make sure they fall through to the `ContextMenuContainer`: https://github.com/ppy/osu/blob/19f892687a0607afbe4e0d010366dc2a66236073/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs#L122-L126 But because the whole editor is sharing a `ContextMenuContainer` and it's at a higher level than both components, we observe here the playfield's `SelectionHandler` intercepting the right click before it can reach the `ContextMenuContainer`. The fix here is similar to what we're already doing in `TimelineBlueprintContaienr`. --- .../Compose/Components/ComposeBlueprintContainer.cs | 5 ++++- osu.Game/Screens/Edit/EditorScreenWithTimeline.cs | 13 +++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index c8cfac454a..ba570a9251 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -40,11 +40,14 @@ namespace osu.Game.Screens.Edit.Compose.Components public PlacementBlueprint CurrentPlacement { get; private set; } + [Resolved] + private EditorScreenWithTimeline editorScreen { get; set; } + /// /// Positional input must be received outside the container's bounds, /// in order to handle composer blueprints which are partially offscreen. /// - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen.MainContent.ReceivePositionalInputAt(screenSpacePos); public ComposeBlueprintContainer(HitObjectComposer composer) : base(composer) diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index ea2790b50a..e1ec1ad4ac 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -11,13 +11,14 @@ using osu.Game.Screens.Edit.Compose.Components.Timeline; namespace osu.Game.Screens.Edit { + [Cached] public abstract partial class EditorScreenWithTimeline : EditorScreen { public const float PADDING = 10; - private Container timelineContainer = null!; + public Container TimelineContent = null!; - private Container mainContent = null!; + public Container MainContent = null!; private LoadingSpinner spinner = null!; @@ -70,7 +71,7 @@ namespace osu.Game.Screens.Edit { new Drawable[] { - timelineContainer = new Container + TimelineContent = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -93,7 +94,7 @@ namespace osu.Game.Screens.Edit }, new Drawable[] { - mainContent = new Container + MainContent = new Container { Name = "Main content", RelativeSizeAxes = Axes.Both, @@ -116,10 +117,10 @@ namespace osu.Game.Screens.Edit { spinner.State.Value = Visibility.Hidden; - mainContent.Add(content); + MainContent.Add(content); content.FadeInFromZero(300, Easing.OutQuint); - LoadComponentAsync(new TimelineArea(CreateTimelineContent()), timelineContainer.Add); + LoadComponentAsync(new TimelineArea(CreateTimelineContent()), TimelineContent.Add); }); } From 63e6eaf53880e326d43a75efec66134bb36bfe30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Oct 2023 17:55:21 +0900 Subject: [PATCH 2080/2100] Fix failing tests --- osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs | 2 +- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs index 0051488029..d8219ff36e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs @@ -182,7 +182,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("click away", () => { - InputManager.MoveMouseTo(Editor.ChildrenOfType().Single().ScreenSpaceDrawQuad.TopLeft + Vector2.One); + InputManager.MoveMouseTo(Editor.ChildrenOfType().First().ScreenSpaceDrawQuad.TopLeft + new Vector2(5)); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index ba570a9251..c7c7c4aa83 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -40,14 +40,14 @@ namespace osu.Game.Screens.Edit.Compose.Components public PlacementBlueprint CurrentPlacement { get; private set; } - [Resolved] + [Resolved(canBeNull: true)] private EditorScreenWithTimeline editorScreen { get; set; } /// /// Positional input must be received outside the container's bounds, /// in order to handle composer blueprints which are partially offscreen. /// - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen.MainContent.ReceivePositionalInputAt(screenSpacePos); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen?.MainContent.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos); public ComposeBlueprintContainer(HitObjectComposer composer) : base(composer) From 0ed5f274f6e45ea22401a50e905e2a1d3406f170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 10:48:31 +0100 Subject: [PATCH 2081/2100] Enable NRT in `TestSceneSliderVelocityAdjust` --- .../Editor/TestSceneSliderVelocityAdjust.cs | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs index bb8c52bdfc..d92c6ebb60 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Diagnostics; using System.Linq; using NUnit.Framework; @@ -24,15 +22,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public partial class TestSceneSliderVelocityAdjust : OsuGameTestScene { - private Screens.Edit.Editor editor => Game.ScreenStack.CurrentScreen as Screens.Edit.Editor; + private Screens.Edit.Editor? editor => Game.ScreenStack.CurrentScreen as Screens.Edit.Editor; - private EditorBeatmap editorBeatmap => editor.ChildrenOfType().FirstOrDefault(); + private EditorBeatmap editorBeatmap => editor.ChildrenOfType().FirstOrDefault()!; - private EditorClock editorClock => editor.ChildrenOfType().FirstOrDefault(); + private EditorClock editorClock => editor.ChildrenOfType().FirstOrDefault()!; - private Slider slider => editorBeatmap.HitObjects.OfType().FirstOrDefault(); + private Slider? slider => editorBeatmap.HitObjects.OfType().FirstOrDefault(); - private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType().FirstOrDefault(); + private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType().FirstOrDefault()!; private DifficultyPointPiece difficultyPointPiece => blueprint.ChildrenOfType().First(); @@ -66,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("ensure one slider placed", () => slider != null); - AddStep("store velocity", () => velocity = slider.Velocity); + AddStep("store velocity", () => velocity = slider!.Velocity); if (adjustVelocity) { @@ -76,10 +74,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("velocity adjusted", () => { Debug.Assert(velocity != null); - return Precision.AlmostEquals(velocity.Value * 2, slider.Velocity); + return Precision.AlmostEquals(velocity.Value * 2, slider!.Velocity); }); - AddStep("store velocity", () => velocity = slider.Velocity); + AddStep("store velocity", () => velocity = slider!.Velocity); } AddStep("save", () => InputManager.Keys(PlatformAction.Save)); @@ -88,8 +86,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader())); AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true); - AddStep("seek to slider", () => editorClock.Seek(slider.StartTime)); - AddAssert("slider has correct velocity", () => slider.Velocity == velocity); + AddStep("seek to slider", () => editorClock.Seek(slider!.StartTime)); + AddAssert("slider has correct velocity", () => slider!.Velocity == velocity); } } } From e1ff0d12c66db4c2f16bc5d88fdb568dd5341bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 10:55:26 +0100 Subject: [PATCH 2082/2100] Update tests to NUnit-style assertions --- .../Editor/TestSceneSliderVelocityAdjust.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs index d92c6ebb60..979801bc41 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs @@ -1,7 +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.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Input; @@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor double? velocity = null; AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader())); - AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true); + AddUntilStep("wait for editor load", () => editor?.ReadyForUse, () => Is.True); AddStep("seek to first control point", () => editorClock.Seek(editorBeatmap.ControlPointInfo.TimingPoints.First().Time)); AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3)); @@ -58,11 +57,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("exit placement mode", () => InputManager.Key(Key.Number1)); - AddAssert("slider placed", () => slider != null); + AddAssert("slider placed", () => slider, () => Is.Not.Null); AddStep("select slider", () => editorBeatmap.SelectedHitObjects.Add(slider)); - AddAssert("ensure one slider placed", () => slider != null); + AddAssert("ensure one slider placed", () => slider, () => Is.Not.Null); AddStep("store velocity", () => velocity = slider!.Velocity); @@ -71,11 +70,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("open velocity adjust panel", () => difficultyPointPiece.TriggerClick()); AddStep("change velocity", () => velocityTextBox.Current.Value = 2); - AddAssert("velocity adjusted", () => - { - Debug.Assert(velocity != null); - return Precision.AlmostEquals(velocity.Value * 2, slider!.Velocity); - }); + AddAssert("velocity adjusted", () => slider!.Velocity, + () => Is.EqualTo(velocity!.Value * 2).Within(Precision.DOUBLE_EPSILON)); AddStep("store velocity", () => velocity = slider!.Velocity); } @@ -84,10 +80,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("exit", () => InputManager.Key(Key.Escape)); AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader())); - AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true); + AddUntilStep("wait for editor load", () => editor?.ReadyForUse, () => Is.True); AddStep("seek to slider", () => editorClock.Seek(slider!.StartTime)); - AddAssert("slider has correct velocity", () => slider!.Velocity == velocity); + AddAssert("slider has correct velocity", () => slider!.Velocity, () => Is.EqualTo(velocity)); } } } From b3369dbb7b188d99f226894924a70339478c21b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 10:57:48 +0100 Subject: [PATCH 2083/2100] Add failing test for slider velocity --- .../Editor/TestSceneSliderVelocityAdjust.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs index 979801bc41..175cbeca6e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs @@ -85,5 +85,50 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("seek to slider", () => editorClock.Seek(slider!.StartTime)); AddAssert("slider has correct velocity", () => slider!.Velocity, () => Is.EqualTo(velocity)); } + + [Test] + public void TestVelocityUndo() + { + double? velocityBefore = null; + double? durationBefore = null; + + AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader())); + AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true); + + AddStep("seek to first control point", () => editorClock.Seek(editorBeatmap.ControlPointInfo.TimingPoints.First().Time)); + AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3)); + + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(editor.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre)); + AddStep("start placement", () => InputManager.Click(MouseButton.Left)); + + AddStep("move mouse to bottom right", () => InputManager.MoveMouseTo(editor.ChildrenOfType().First().ScreenSpaceDrawQuad.BottomRight - new Vector2(10))); + AddStep("end placement", () => InputManager.Click(MouseButton.Right)); + + AddStep("exit placement mode", () => InputManager.Key(Key.Number1)); + + AddAssert("slider placed", () => slider, () => Is.Not.Null); + AddStep("select slider", () => editorBeatmap.SelectedHitObjects.Add(slider)); + + AddStep("store velocity", () => + { + velocityBefore = slider!.Velocity; + durationBefore = slider.Duration; + }); + + AddStep("open velocity adjust panel", () => difficultyPointPiece.TriggerClick()); + AddStep("change velocity", () => velocityTextBox.Current.Value = 2); + + AddAssert("velocity adjusted", () => slider!.Velocity, () => Is.EqualTo(velocityBefore!.Value * 2).Within(Precision.DOUBLE_EPSILON)); + + AddStep("undo", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Z); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + AddAssert("slider has correct velocity", () => slider!.Velocity, () => Is.EqualTo(velocityBefore)); + AddAssert("slider has correct duration", () => slider!.Duration, () => Is.EqualTo(durationBefore)); + } } } From de89b7e53c305c0bf048b3ee42c028775d2da2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 10:59:02 +0100 Subject: [PATCH 2084/2100] Fix slider velocity changes not being undone correctly Closes https://github.com/ppy/osu/issues/25239. `LegacyEditorBeatmapPatcher.processHitObjectLocalData()` was already supposed to be handling changes to hitobjects that will show up neither when comparing the hitobjects themselves or the timing point with "legacy" info stripped - so, in other words, changes to slider velocity and samples. However, a change to slider velocity requires default application to take effect, so just resetting the value would visually fix the timeline marker but not change the actual object. Calling `EditorBeatmap.Update()` fixes this by way of triggering default re-application. This could probably be smarter (by only invoking the update when strictly necessary, etc.) - but I'm not sure it's worth the hassle. This is intended to be a quick fix, rather than a complete solution - the complete solution would indeed likely entail a wholesale restructuring of the editor's change handling. --- osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index fe0d4a7822..bb9f702cb5 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -123,6 +123,8 @@ namespace osu.Game.Screens.Edit oldWithRepeats.NodeSamples.Clear(); oldWithRepeats.NodeSamples.AddRange(newWithRepeats.NodeSamples); } + + editorBeatmap.Update(oldObject); } } From cea24298cb99c901077c6e8a23bdfa34da3ceafd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 12:42:34 +0100 Subject: [PATCH 2085/2100] Privatise setters --- osu.Game/Screens/Edit/EditorScreenWithTimeline.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index e1ec1ad4ac..575a66d421 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -16,9 +16,9 @@ namespace osu.Game.Screens.Edit { public const float PADDING = 10; - public Container TimelineContent = null!; + public Container TimelineContent { get; private set; } = null!; - public Container MainContent = null!; + public Container MainContent { get; private set; } = null!; private LoadingSpinner spinner = null!; From 88e10dd051d33ca0e63a7cd5f82dac3d9b4e039c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 20:03:44 +0100 Subject: [PATCH 2086/2100] Update framework --- 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 0575817460..2870696c03 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 9b06b4a6a7..f1159f58b9 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 06508d08fe1f289404bb20756f43403c209e0d0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 20:22:41 +0100 Subject: [PATCH 2087/2100] Delete outdated test --- .../UserInterface/TestSceneSearchTextBox.cs | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneSearchTextBox.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSearchTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSearchTextBox.cs deleted file mode 100644 index 153525d24a..0000000000 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSearchTextBox.cs +++ /dev/null @@ -1,38 +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 NUnit.Framework; -using osu.Framework.Graphics; -using osu.Game.Graphics.UserInterface; -using osuTK; - -namespace osu.Game.Tests.Visual.UserInterface -{ - public partial class TestSceneSearchTextBox : OsuTestScene - { - private SearchTextBox textBox = null!; - - [SetUp] - public void SetUp() => Schedule(() => - { - Child = textBox = new SearchTextBox - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 400, - Scale = new Vector2(2f), - HoldFocus = true, - }; - }); - - [Test] - public void TestSelectionOnFocus() - { - AddStep("set text", () => textBox.Text = "some text"); - AddAssert("no text selected", () => textBox.SelectedText == string.Empty); - AddStep("hide text box", () => textBox.Hide()); - AddStep("show text box", () => textBox.Show()); - AddAssert("search text selected", () => textBox.SelectedText == textBox.Text); - } - } -} From f2c0bc821802067e8cb9c7e5bbf64215e6e4fc31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 21:15:04 +0100 Subject: [PATCH 2088/2100] Add failing test case --- .../TestSceneSpinnerInput.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs index 5a473409a4..a6c15d5a67 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.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.Linq; using NUnit.Framework; @@ -10,6 +11,8 @@ using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Replays; @@ -47,6 +50,7 @@ namespace osu.Game.Rulesets.Osu.Tests public void Setup() => Schedule(() => { manualClock = null; + SelectedMods.Value = Array.Empty(); }); /// @@ -102,6 +106,34 @@ namespace osu.Game.Rulesets.Osu.Tests assertSpinnerHit(false); } + [Test] + public void TestVibrateWithoutSpinningOnCentreWithDoubleTime() + { + List frames = new List(); + + const int rate = 2; + // the track clock is going to be playing twice as fast, + // so the vibration time in clock time needs to be twice as long + // to keep constant speed in real time. + const int vibrate_time = 50 * rate; + + int direction = -1; + + for (double i = time_spinner_start; i <= time_spinner_end; i += vibrate_time) + { + frames.Add(new OsuReplayFrame(i, new Vector2(centre_x + direction * 50, centre_y), OsuAction.LeftButton)); + frames.Add(new OsuReplayFrame(i + vibrate_time, new Vector2(centre_x - direction * 50, centre_y), OsuAction.LeftButton)); + + direction *= -1; + } + + AddStep("set DT", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = rate } } }); + performTest(frames); + + assertTicksHit(0); + assertSpinnerHit(false); + } + /// /// Spins in a single direction. /// From e5b51f769ce72e7394131df3b993a7a840115984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 21:15:40 +0100 Subject: [PATCH 2089/2100] Fix incorrect assertion placement in spinner rotation tracker Checking the delta after the application of rate is not correct. The delta is in screen-space *before* the rate from rate-changing mods were applied; the point of the application of the rate is to compensate for the fact that the spinner is still judged in "track time" - but the goal is to keep the spinner's difficulty *independent* of rate, which means that with DT active the user's spin is "twice as effective" to compensate for the fact that the spinner is twice as short in real time. In another formulation, with DT active, the user gets to record replay frames "half as often" as in normal gameplay. --- .../Skinning/Default/SpinnerRotationTracker.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index 374f3f461b..1d75663fd9 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -101,11 +101,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default rotationTransferred = true; } + Debug.Assert(Math.Abs(delta) <= 180); + double rate = gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate; delta = (float)(delta * Math.Abs(rate)); - Debug.Assert(Math.Abs(delta) <= 180); - currentRotation += delta; drawableSpinner.Result.History.ReportDelta(Time.Current, delta); } From 12ef93ac3b42a86c031c37a9f89e430bf90912b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 21:31:34 +0100 Subject: [PATCH 2090/2100] Remove no-longer-valid assertion --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs index a6c15d5a67..75bcd809c8 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs @@ -130,7 +130,6 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("set DT", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = rate } } }); performTest(frames); - assertTicksHit(0); assertSpinnerHit(false); } From 87c9df937f85d3f7e9b220c2f21c13caae74b25b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 12:40:21 +0900 Subject: [PATCH 2091/2100] Move team seed to below team name --- .../Components/DrawableTeamSeed.cs | 6 +++++ .../TournamentSpriteTextWithBackground.cs | 2 +- .../Gameplay/Components/TeamDisplay.cs | 27 ++++++------------- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tournament/Components/DrawableTeamSeed.cs b/osu.Game.Tournament/Components/DrawableTeamSeed.cs index a79c63e979..077185f5c0 100644 --- a/osu.Game.Tournament/Components/DrawableTeamSeed.cs +++ b/osu.Game.Tournament/Components/DrawableTeamSeed.cs @@ -22,6 +22,12 @@ namespace osu.Game.Tournament.Components [Resolved] private LadderInfo ladder { get; set; } = null!; + [BackgroundDependencyLoader] + private void load() + { + Text.Font = Text.Font.With(size: 36); + } + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs index ce118727cd..21439482e3 100644 --- a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs +++ b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tournament.Components { Colour = TournamentGame.ELEMENT_FOREGROUND_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.SemiBold, size: 50), - Padding = new MarginPadding { Horizontal = 10 }, + Padding = new MarginPadding { Left = 10, Right = 20 }, Text = text, } }; diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index 49fbc64397..3eec67c639 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -95,28 +95,17 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components } } }, - new FillFlowContainer + teamNameText = new TournamentSpriteTextWithBackground { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), + Scale = new Vector2(0.5f), + Origin = anchor, + Anchor = anchor, + }, + new DrawableTeamSeed(Team) + { + Scale = new Vector2(0.5f), Origin = anchor, Anchor = anchor, - Children = new Drawable[] - { - teamNameText = new TournamentSpriteTextWithBackground - { - Scale = new Vector2(0.5f), - Origin = anchor, - Anchor = anchor, - }, - new DrawableTeamSeed(Team) - { - Scale = new Vector2(0.5f), - Origin = anchor, - Anchor = anchor, - }, - } }, } }, From feeb95e4c389b85a1e72b1f00d2a79131ecb2652 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 12:44:43 +0900 Subject: [PATCH 2092/2100] Adjust `DrawableTeamTitleWithHeader` to match new layout --- .../Components/DrawableTeamTitleWithHeader.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs index fc4037d4e1..7d8fc847d4 100644 --- a/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs +++ b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs @@ -18,21 +18,12 @@ namespace osu.Game.Tournament.Components { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 10), + Spacing = new Vector2(0, 5), Children = new Drawable[] { new DrawableTeamHeader(colour), - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - Children = new Drawable[] - { - new DrawableTeamTitle(team), - new DrawableTeamSeed(team), - } - } + new DrawableTeamTitle(team), + new DrawableTeamSeed(team), } }; } From a3dc9a73b19120c0c4ef4ba7199463fb63ad280c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 13:35:15 +0900 Subject: [PATCH 2093/2100] Revert behaviour changes of `MaxDimensions` test and ignore instead --- .../Gameplay/TestScenePlayerMaxDimensions.cs | 76 ++++++++++++++++--- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs index 6665295e99..53a4abdd07 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs @@ -3,14 +3,21 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Screens.Play; using osu.Game.Skinning; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; namespace osu.Game.Tests.Visual.Gameplay { @@ -21,6 +28,7 @@ namespace osu.Game.Tests.Visual.Gameplay /// /// The HUD is hidden as it does't really affect game balance if HUD elements are larger than they should be. /// + [Ignore("This test is for visual testing, and has no value in being run in standard CI runs.")] public partial class TestScenePlayerMaxDimensions : TestSceneAllRulesetPlayers { // scale textures to 4 times their size. @@ -66,18 +74,66 @@ namespace osu.Game.Tests.Visual.Gameplay remove { } } - public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) - { - var texture = base.GetTexture(componentName, wrapModeS, wrapModeT); - - if (texture != null) - texture.ScaleAdjust /= scale_factor; - - return texture; - } - public ISkin FindProvider(Func lookupFunction) => this; public IEnumerable AllSources => new[] { this }; + + protected override IResourceStore CreateTextureLoaderStore(IStorageResourceProvider resources, IResourceStore storage) + => new UpscaledTextureLoaderStore(base.CreateTextureLoaderStore(resources, storage)); + + private class UpscaledTextureLoaderStore : IResourceStore + { + private readonly IResourceStore? textureStore; + + public UpscaledTextureLoaderStore(IResourceStore? textureStore) + { + this.textureStore = textureStore; + } + + public void Dispose() + { + textureStore?.Dispose(); + } + + public TextureUpload Get(string name) + { + var textureUpload = textureStore?.Get(name); + + // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp. + if (textureUpload == null) + return null!; + + return upscale(textureUpload); + } + + public async Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) + { + // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp. + if (textureStore == null) + return null!; + + var textureUpload = await textureStore.GetAsync(name, cancellationToken).ConfigureAwait(false); + + if (textureUpload == null) + return null!; + + return await Task.Run(() => upscale(textureUpload), cancellationToken).ConfigureAwait(false); + } + + private TextureUpload upscale(TextureUpload textureUpload) + { + var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height); + + // The original texture upload will no longer be returned or used. + textureUpload.Dispose(); + + image.Mutate(i => i.Resize(new Size(textureUpload.Width, textureUpload.Height) * scale_factor)); + return new TextureUpload(image); + } + + public Stream? GetStream(string name) => textureStore?.GetStream(name); + + public IEnumerable GetAvailableResources() => textureStore?.GetAvailableResources() ?? Array.Empty(); + } } } } From 37ec10d4f592ebab514c9a9afc5f9dcb8dce7c2f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 13:49:39 +0900 Subject: [PATCH 2094/2100] Fix `TestSongSelectScrollHandling` not waiting for `VolumeOverlay` to load See https://github.com/ppy/osu/actions/runs/6701786492/job/18210372721. --- osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 7fa4f8c836..9e743ef336 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -175,7 +175,7 @@ namespace osu.Game.Tests.Visual.Navigation double scrollPosition = 0; AddStep("set game volume to max", () => Game.Dependencies.Get().SetValue(FrameworkSetting.VolumeUniversal, 1d)); - AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); + AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType().SingleOrDefault()?.State.Value, () => Is.EqualTo(Visibility.Hidden)); PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); From 89444d5544aad8c89be4eff311cd6beeedc1c421 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 14:00:49 +0900 Subject: [PATCH 2095/2100] Fix export test still occasionally failing due to file write in progress https://github.com/ppy/osu/actions/runs/6701591401/job/18209826074 Basically, `File.Move` may not be an atomic operation. --- .../Gameplay/TestScenePlayerLocalScoreImport.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs index 1254aa0639..0dd544bb30 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs @@ -214,10 +214,18 @@ namespace osu.Game.Tests.Visual.Gameplay // Files starting with _ are temporary, created by CreateFileSafely call. AddUntilStep("wait for export file", () => filePath = LocalStorage.GetFiles("exports").SingleOrDefault(f => !Path.GetFileName(f).StartsWith("_", StringComparison.Ordinal)), () => Is.Not.Null); - AddAssert("filesize is non-zero", () => + AddUntilStep("filesize is non-zero", () => { - using (var stream = LocalStorage.GetStream(filePath)) - return stream.Length; + try + { + using (var stream = LocalStorage.GetStream(filePath)) + return stream.Length; + } + catch (IOException) + { + // file move may still be in progress. + return 0; + } }, () => Is.Not.Zero); } From 66b84d02cb1631302631d5b16afd31d0d420a13a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 14:20:11 +0900 Subject: [PATCH 2096/2100] Add note about `TestGameplayExitFlow` failure, and ignore for now See: https://github.com/ppy/osu/actions/runs/6695995685/job/18194110641 https://github.com/ppy/osu/actions/runs/6700910613/job/18208272419 --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 09624f63b7..16030d568b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -693,7 +693,9 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] - [FlakyTest] // See above + [Ignore("Failing too often, needs revisiting in some future.")] + // This test is failing even after 10 retries (see https://github.com/ppy/osu/actions/runs/6700910613/job/18208272419) + // Something is stopping the ready button from changing states, over multiple runs. public void TestGameplayExitFlow() { Bindable? holdDelay = null; From bdd3f2847b8a27d1e34657297fdc290deeaf1e88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 14:26:00 +0900 Subject: [PATCH 2097/2100] Add an extra storyboard sample to avoid intermittent failures in `TestStoryboardSamplesStopOnSkip` Probably CI running slow timing balls. The point of failure is `waitUntilStoryboardSamplesPlay()` after already testing the important part of the test (that the samples stop on skip) so let's give it another possible point to recover. See https://github.com/ppy/osu/actions/runs/6698399814/job/18201753701. --- .../Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs index a9d4508f70..11dc0f9c30 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs @@ -41,6 +41,7 @@ namespace osu.Game.Tests.Visual.Gameplay backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: -7000, volume: 20)); backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: -5000, volume: 20)); backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 0, volume: 20)); + backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 2000, volume: 20)); } [SetUp] From d379e553da920cfd8cbccca2f5a03786c8627dc6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 14:31:26 +0900 Subject: [PATCH 2098/2100] Fix back-to-front logging --- osu.Game.Tests/Visual/Gameplay/TestScenePause.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index b072ce191e..29c9381ee4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Gameplay alwaysGoingForward &= goingForward; if (!goingForward) - Logger.Log($"Backwards time occurred ({currentTime:N1} -> {lastTime:N1})"); + Logger.Log($"Backwards time occurred ({lastTime:N1} -> {currentTime:N1})"); lastTime = currentTime; }; From 7ceced70122d51ffc030074d1f3738645367be59 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 14:47:04 +0900 Subject: [PATCH 2099/2100] Scope `TestPauseWithLargeOffset` to focus on what matters See https://github.com/ppy/osu/actions/runs/6693917410/job/18186111009 This test is to make sure we don't seek before the original pause time, so I've exposed that value precisely to avoid CI woes. --- osu.Game.Tests/Visual/Gameplay/TestScenePause.cs | 16 ++++++++++------ .../Screens/Play/MasterGameplayClockContainer.cs | 12 ++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 29c9381ee4..ec3b3e0822 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestPauseWithLargeOffset() { - double lastTime; + double lastStopTime; bool alwaysGoingForward = true; AddStep("force large offset", () => @@ -84,20 +84,24 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("add time forward check hook", () => { - lastTime = double.MinValue; + lastStopTime = double.MinValue; alwaysGoingForward = true; Player.OnUpdate += _ => { - double currentTime = Player.GameplayClockContainer.CurrentTime; - bool goingForward = currentTime >= lastTime - 500; + var masterClock = (MasterGameplayClockContainer)Player.GameplayClockContainer; + + double currentTime = masterClock.CurrentTime; + + bool goingForward = currentTime >= (masterClock.LastStopTime ?? lastStopTime); alwaysGoingForward &= goingForward; if (!goingForward) - Logger.Log($"Backwards time occurred ({lastTime:N1} -> {currentTime:N1})"); + Logger.Log($"Went too far backwards (last stop: {lastStopTime:N1} current: {currentTime:N1})"); - lastTime = currentTime; + if (masterClock.LastStopTime != null) + lastStopTime = masterClock.LastStopTime.Value; }; }); diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 1c860e9d4b..54ed7ba626 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.Play /// /// In the future I want to change this. /// - private double? actualStopTime; + internal double? LastStopTime; [Resolved] private MusicController musicController { get; set; } = null!; @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Play protected override void StopGameplayClock() { - actualStopTime = GameplayClock.CurrentTime; + LastStopTime = GameplayClock.CurrentTime; if (IsLoaded) { @@ -127,17 +127,17 @@ namespace osu.Game.Screens.Play public override void Seek(double time) { // Safety in case the clock is seeked while stopped. - actualStopTime = null; + LastStopTime = null; base.Seek(time); } protected override void PrepareStart() { - if (actualStopTime != null) + if (LastStopTime != null) { - Seek(actualStopTime.Value); - actualStopTime = null; + Seek(LastStopTime.Value); + LastStopTime = null; } else base.PrepareStart(); From 8c067dc584066527badb6a9029f5ef31a932bba8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 14:53:07 +0900 Subject: [PATCH 2100/2100] Fix mod tests not waiting for presets to finish loading See https://github.com/ppy/osu/actions/runs/6692350567/job/18181352850. --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 3728fb3f21..f0822ce2a8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -799,8 +799,11 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(0.7)); } - private void waitForColumnLoad() => AddUntilStep("all column content loaded", - () => modSelectOverlay.ChildrenOfType().Any() && modSelectOverlay.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded)); + private void waitForColumnLoad() => AddUntilStep("all column content loaded", () => + modSelectOverlay.ChildrenOfType().Any() + && modSelectOverlay.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded) + && modSelectOverlay.ChildrenOfType().Any() + && modSelectOverlay.ChildrenOfType().All(column => column.IsLoaded)); private void changeRuleset(int id) {