From f1535b74beb185c83f76d7e7e3a54e55b6c32a81 Mon Sep 17 00:00:00 2001 From: Kaleb Date: Sun, 13 Feb 2022 02:16:06 -0500 Subject: [PATCH 01/19] Give Spun Out mod dynamic spin rate --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 098c639949..b900fa3274 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -45,7 +45,8 @@ namespace osu.Game.Rulesets.Osu.Mods // for that reason using ElapsedFrameTime directly leads to fewer SPM with Half Time and more SPM with Double Time. // for spinners we want the real (wall clock) elapsed time; to achieve that, unapply the clock rate locally here. double rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate; - spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * 0.03f)); + float rotationSpeed = (float)(spinner.HitObject.SpinsRequired / spinner.HitObject.Duration / 1.01); + spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f)); } } } From 585bd541f319ce316c8c4eef80aeb4247449b232 Mon Sep 17 00:00:00 2001 From: Kaleb Date: Sun, 13 Feb 2022 02:38:49 -0500 Subject: [PATCH 02/19] Add missing parentheses to RPM calculation --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index b900fa3274..4725a43a77 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Mods // for that reason using ElapsedFrameTime directly leads to fewer SPM with Half Time and more SPM with Double Time. // for spinners we want the real (wall clock) elapsed time; to achieve that, unapply the clock rate locally here. double rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate; - float rotationSpeed = (float)(spinner.HitObject.SpinsRequired / spinner.HitObject.Duration / 1.01); + float rotationSpeed = (float)(spinner.HitObject.SpinsRequired / (spinner.HitObject.Duration / 1.01)); spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f)); } } From df9535d195700205380bc624f2a9999cfa9e228c Mon Sep 17 00:00:00 2001 From: Kaleb Date: Sun, 13 Feb 2022 14:28:40 -0500 Subject: [PATCH 03/19] Update RPM calculation for readability Multiply the 1.01 factor to the resulting RPM, not to the duration. --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 4725a43a77..9be0dc748a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -45,7 +45,10 @@ namespace osu.Game.Rulesets.Osu.Mods // for that reason using ElapsedFrameTime directly leads to fewer SPM with Half Time and more SPM with Double Time. // for spinners we want the real (wall clock) elapsed time; to achieve that, unapply the clock rate locally here. double rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate; - float rotationSpeed = (float)(spinner.HitObject.SpinsRequired / (spinner.HitObject.Duration / 1.01)); + + // multiply the SPM by 1.01 to ensure that the spinner is completed. if the calculation is left exact, + // some spinners may not complete due to very minor decimal loss during calculation + float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration); spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f)); } } From c1777f20e114da75a51c6e3fc38ad5f637b4900d Mon Sep 17 00:00:00 2001 From: Kaleb Date: Mon, 14 Feb 2022 03:11:44 -0500 Subject: [PATCH 04/19] Fix Spun Out tests Change 'unaffected by mods' test to use dynamic RPM value instead of a fixed value --- .../Mods/TestSceneOsuModSpunOut.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index 24e69703a6..29d7e7b4d6 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -48,7 +48,19 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods PassCondition = () => { var counter = Player.ChildrenOfType().SingleOrDefault(); - return counter != null && Precision.AlmostEquals(counter.Result.Value, 286, 1); + var spinner = Player.ChildrenOfType().FirstOrDefault(); + + if (counter == null || spinner == null) + return false; + + // ignore cases where the spinner hasn't started as these lead to false-positives + if (Precision.AlmostEquals(counter.Result.Value, 0, 1)) + return false; + + double rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate; + float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration); + + return Precision.AlmostEquals(counter.Result.Value, rotationSpeed * 1000 * 60, 1); } }); } From 95b1bffffeceff678762ccb32e329a0b9277af6a Mon Sep 17 00:00:00 2001 From: Kaleb Date: Mon, 14 Feb 2022 03:45:02 -0500 Subject: [PATCH 05/19] Add test to ensure spinners only complete No bonus or a non-300 judgement --- .../Mods/TestSceneOsuModSpunOut.cs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index 29d7e7b4d6..e71377a505 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods @@ -57,7 +58,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods if (Precision.AlmostEquals(counter.Result.Value, 0, 1)) return false; - double rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate; float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration); return Precision.AlmostEquals(counter.Result.Value, rotationSpeed * 1000 * 60, 1); @@ -65,6 +65,27 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods }); } + [Test] + public void TestSpinnerOnlyComplete() => CreateModTest(new ModTestData + { + Mod = new OsuModSpunOut(), + Autoplay = false, + Beatmap = singleSpinnerBeatmap, + PassCondition = () => + { + var spinner = Player.ChildrenOfType().SingleOrDefault(); + var gameplayClockContainer = Player.ChildrenOfType().SingleOrDefault(); + + if (spinner == null || gameplayClockContainer == null) + return false; + + if (!Precision.AlmostEquals(gameplayClockContainer.CurrentTime, spinner.HitObject.StartTime + spinner.HitObject.Duration, 200.0f)) + return false; + + return Precision.AlmostEquals(spinner.Progress, 1.0f, 0.05f) && Precision.AlmostEquals(spinner.GainedBonus.Value, 0, 1); + } + }); + private Beatmap singleSpinnerBeatmap => new Beatmap { HitObjects = new List From 98aaf83177ee1cb8777619936e31926b2167b399 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Feb 2022 15:57:37 +0900 Subject: [PATCH 06/19] Add a centralised constant for the osu URL schema protocol --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 12 ++++++------ osu.Game.Tests/Visual/Online/TestSceneChatLink.cs | 4 ++-- osu.Game/Online/Chat/MessageFormatter.cs | 4 ++-- osu.Game/OsuGameBase.cs | 2 ++ 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 8def8005f1..cea4d510c1 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -409,26 +409,26 @@ namespace osu.Game.Tests.Chat Assert.AreEqual(result.Content, result.DisplayContent); Assert.AreEqual(2, result.Links.Count); - Assert.AreEqual("osu://chan/#english", result.Links[0].Url); - Assert.AreEqual("osu://chan/#japanese", result.Links[1].Url); + Assert.AreEqual($"{OsuGameBase.OSU_PROTOCOL}chan/#english", result.Links[0].Url); + Assert.AreEqual($"{OsuGameBase.OSU_PROTOCOL}chan/#japanese", result.Links[1].Url); } [Test] public void TestOsuProtocol() { - Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a custom protocol osu://chan/#english." }); + Message result = MessageFormatter.FormatMessage(new Message { Content = $"This is a custom protocol {OsuGameBase.OSU_PROTOCOL}chan/#english." }); Assert.AreEqual(result.Content, result.DisplayContent); Assert.AreEqual(1, result.Links.Count); - Assert.AreEqual("osu://chan/#english", result.Links[0].Url); + Assert.AreEqual($"{OsuGameBase.OSU_PROTOCOL}chan/#english", result.Links[0].Url); Assert.AreEqual(26, result.Links[0].Index); Assert.AreEqual(19, result.Links[0].Length); - result = MessageFormatter.FormatMessage(new Message { Content = "This is a [custom protocol](osu://chan/#english)." }); + result = MessageFormatter.FormatMessage(new Message { Content = $"This is a [custom protocol]({OsuGameBase.OSU_PROTOCOL}chan/#english)." }); Assert.AreEqual("This is a custom protocol.", result.DisplayContent); Assert.AreEqual(1, result.Links.Count); - Assert.AreEqual("osu://chan/#english", result.Links[0].Url); + Assert.AreEqual($"{OsuGameBase.OSU_PROTOCOL}chan/#english", result.Links[0].Url); Assert.AreEqual("#english", result.Links[0].Argument); Assert.AreEqual(10, result.Links[0].Index); Assert.AreEqual(15, result.Links[0].Length); diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index 12b5f64559..d077868175 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -87,8 +87,8 @@ namespace osu.Game.Tests.Visual.Online addMessageWithChecks("likes to post this [https://dev.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External); addMessageWithChecks("Join my multiplayer game osump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch); addMessageWithChecks("Join my [multiplayer game](osump://12346).", 1, expectedActions: LinkAction.JoinMultiplayerMatch); - addMessageWithChecks("Join my [#english](osu://chan/#english).", 1, expectedActions: LinkAction.OpenChannel); - addMessageWithChecks("Join my osu://chan/#english.", 1, expectedActions: LinkAction.OpenChannel); + addMessageWithChecks($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", 1, expectedActions: LinkAction.OpenChannel); + 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); diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index d7974004b1..4c477d58b6 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -269,10 +269,10 @@ namespace osu.Game.Online.Chat handleAdvanced(advanced_link_regex, result, startIndex); // handle editor times - handleMatches(time_regex, "{0}", "osu://edit/{0}", result, startIndex, LinkAction.OpenEditorTimestamp); + handleMatches(time_regex, "{0}", $"{OsuGameBase.OSU_PROTOCOL}edit/{0}", result, startIndex, LinkAction.OpenEditorTimestamp); // handle channels - handleMatches(channel_regex, "{0}", "osu://chan/{0}", result, startIndex, LinkAction.OpenChannel); + handleMatches(channel_regex, "{0}", $"{OsuGameBase.OSU_PROTOCOL}chan/{0}", result, startIndex, LinkAction.OpenChannel); string empty = ""; while (space-- > 0) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 0b2644d5ba..86390e7630 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -52,6 +52,8 @@ namespace osu.Game /// public partial class OsuGameBase : Framework.Game, ICanAcceptFiles { + public const string OSU_PROTOCOL = "osu://"; + public const string CLIENT_STREAM_NAME = @"lazer"; public const int SAMPLE_CONCURRENCY = 6; From 29c5683ba3ce2e7de362cac68949b3a46424cd12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Feb 2022 16:06:38 +0900 Subject: [PATCH 07/19] Add handling of beatmap links on startup --- .../TestSceneStartupBeatmapDisplay.cs | 22 ++++++++++++ .../TestSceneStartupBeatmapSetDisplay.cs | 22 ++++++++++++ osu.Game/OsuGame.cs | 36 ++++++++++++++++++- 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs create mode 100644 osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs new file mode 100644 index 0000000000..1efa24435e --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Overlays; + +namespace osu.Game.Tests.Visual.Navigation +{ + public class TestSceneStartupBeatmapDisplay : OsuGameTestScene + { + protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { "osu://b/75" }); + + [Test] + public void TestBeatmapLink() + { + AddUntilStep("Beatmap overlay displayed", () => Game.ChildrenOfType().FirstOrDefault()?.State.Value == Visibility.Visible); + } + } +} diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs new file mode 100644 index 0000000000..1339c514e4 --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Overlays; + +namespace osu.Game.Tests.Visual.Navigation +{ + public class TestSceneStartupBeatmapSetDisplay : OsuGameTestScene + { + protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { "osu://s/1" }); + + [Test] + public void TestBeatmapSetLink() + { + AddUntilStep("Beatmap overlay displayed", () => Game.ChildrenOfType().FirstOrDefault()?.State.Value == Visibility.Visible); + } + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5b58dec0c3..390e96d768 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -150,6 +150,7 @@ namespace osu.Game protected SettingsOverlay Settings; private VolumeOverlay volume; + private OsuLogo osuLogo; private MainMenu menuScreen; @@ -898,8 +899,41 @@ namespace osu.Game if (args?.Length > 0) { string[] paths = args.Where(a => !a.StartsWith('-')).ToArray(); + if (paths.Length > 0) - Task.Run(() => Import(paths)); + { + string firstPath = paths.First(); + + if (firstPath.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) + { + handleOsuProtocolUrl(firstPath); + } + else + { + Task.Run(() => Import(paths)); + } + } + } + } + + private void handleOsuProtocolUrl(string url) + { + if (!url.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) + throw new ArgumentException("Invalid osu URL provided.", nameof(url)); + + string[] pieces = url.Split('/'); + + switch (pieces[2]) + { + case "s": + if (int.TryParse(pieces[3], out int beatmapSetId)) + ShowBeatmapSet(beatmapSetId); + break; + + case "b": + if (int.TryParse(pieces[3], out int beatmapId)) + ShowBeatmap(beatmapId); + break; } } From 0d56693b7aee6221d16ac2341d86d70b5a805a55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 14:11:22 +0900 Subject: [PATCH 08/19] Fix test not always checking the final bonus value Due to the previous logic not waiting until the spinner had completed, there could be false negatives as the check runs too early, with a potential additional bonus spin occurring afterwards. --- .../Mods/TestSceneOsuModSpunOut.cs | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index e71377a505..cb8eceb213 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -8,13 +8,15 @@ using NUnit.Framework; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Skinning.Default; -using osu.Game.Screens.Play; +using osu.Game.Rulesets.Scoring; using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods @@ -66,25 +68,45 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } [Test] - public void TestSpinnerOnlyComplete() => CreateModTest(new ModTestData + public void TestSpinnerGetsNoBonusScore() { - Mod = new OsuModSpunOut(), - Autoplay = false, - Beatmap = singleSpinnerBeatmap, - PassCondition = () => + DrawableSpinner spinner = null; + List results = new List(); + + CreateModTest(new ModTestData { - var spinner = Player.ChildrenOfType().SingleOrDefault(); - var gameplayClockContainer = Player.ChildrenOfType().SingleOrDefault(); + Mod = new OsuModSpunOut(), + Autoplay = false, + Beatmap = singleSpinnerBeatmap, + PassCondition = () => + { + // Bind to the first spinner's results for further tracking. + if (spinner == null) + { + // We only care about the first spinner we encounter for this test. + var nextSpinner = Player.ChildrenOfType().SingleOrDefault(); - if (spinner == null || gameplayClockContainer == null) - return false; + if (nextSpinner == null) + return false; - if (!Precision.AlmostEquals(gameplayClockContainer.CurrentTime, spinner.HitObject.StartTime + spinner.HitObject.Duration, 200.0f)) - return false; + spinner = nextSpinner; + spinner.OnNewResult += (o, result) => results.Add(result); - return Precision.AlmostEquals(spinner.Progress, 1.0f, 0.05f) && Precision.AlmostEquals(spinner.GainedBonus.Value, 0, 1); - } - }); + results.Clear(); + } + + // we should only be checking the bonus/progress after the spinner has fully completed. + if (!results.OfType().Any(r => r.TimeCompleted != null)) + return false; + + return + results.Any(r => r.Type == HitResult.SmallTickHit) + && !results.Any(r => r.Type == HitResult.LargeTickHit) + && Precision.AlmostEquals(spinner.Progress, 1.0f, 0.05f) + && Precision.AlmostEquals(spinner.GainedBonus.Value, 0, 1); + } + }); + } private Beatmap singleSpinnerBeatmap => new Beatmap { From 91acc9eec6a8859f665f64338420efc564faf33b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 14:36:08 +0900 Subject: [PATCH 09/19] Remove checks which are still going to occasionally fail due to pooling --- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index cb8eceb213..93c4bd96de 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -101,9 +101,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods return results.Any(r => r.Type == HitResult.SmallTickHit) - && !results.Any(r => r.Type == HitResult.LargeTickHit) - && Precision.AlmostEquals(spinner.Progress, 1.0f, 0.05f) - && Precision.AlmostEquals(spinner.GainedBonus.Value, 0, 1); + && !results.Any(r => r.Type == HitResult.LargeTickHit); } }); } From 9e279c3ebc9cdc23ba5d4a1d192642b314019de8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 14:37:52 +0900 Subject: [PATCH 10/19] Fix completely incorrect judgement specification --- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index 93c4bd96de..e61720d8b9 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -100,8 +100,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods return false; return - results.Any(r => r.Type == HitResult.SmallTickHit) - && !results.Any(r => r.Type == HitResult.LargeTickHit); + results.Any(r => r.Type == HitResult.SmallBonus) + && !results.Any(r => r.Type == HitResult.LargeBonus); } }); } From ca0a04115374368184ca8363ac7026384b9c9dcf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 16:45:04 +0900 Subject: [PATCH 11/19] Fix missing escaping causing test failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Online/Chat/MessageFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 4c477d58b6..39a51876c2 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -269,7 +269,7 @@ namespace osu.Game.Online.Chat handleAdvanced(advanced_link_regex, result, startIndex); // handle editor times - handleMatches(time_regex, "{0}", $"{OsuGameBase.OSU_PROTOCOL}edit/{0}", result, startIndex, LinkAction.OpenEditorTimestamp); + handleMatches(time_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp); // handle channels handleMatches(channel_regex, "{0}", $"{OsuGameBase.OSU_PROTOCOL}chan/{0}", result, startIndex, LinkAction.OpenChannel); From ed008267d7a932c64dac5a3566af1bcdd145e415 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 16:45:18 +0900 Subject: [PATCH 12/19] Fix one more case of escaping not being present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Online/Chat/MessageFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 39a51876c2..333c554d44 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -272,7 +272,7 @@ namespace osu.Game.Online.Chat handleMatches(time_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp); // handle channels - handleMatches(channel_regex, "{0}", $"{OsuGameBase.OSU_PROTOCOL}chan/{0}", result, startIndex, LinkAction.OpenChannel); + handleMatches(channel_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}chan/{{0}}", result, startIndex, LinkAction.OpenChannel); string empty = ""; while (space-- > 0) From 5efffa208acc118d8acf16cae9a609318e85b7b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 17:08:09 +0900 Subject: [PATCH 13/19] Add test coverage of beatmap set overlay actually showing requested beatmap --- .../TestSceneStartupBeatmapDisplay.cs | 34 ++++++++++++++++++- .../TestSceneStartupBeatmapSetDisplay.cs | 32 ++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs index 1efa24435e..961b7dedc3 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs @@ -5,18 +5,50 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; +using osu.Game.Overlays.BeatmapSet; namespace osu.Game.Tests.Visual.Navigation { public class TestSceneStartupBeatmapDisplay : OsuGameTestScene { - protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { "osu://b/75" }); + private const int requested_beatmap_id = 75; + private const int requested_beatmap_set_id = 1; + + protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { $"osu://b/{requested_beatmap_id}" }); + + [SetUp] + public void Setup() => Schedule(() => + { + ((DummyAPIAccess)API).HandleRequest = request => + { + switch (request) + { + case GetBeatmapSetRequest gbr: + var apiBeatmapSet = CreateAPIBeatmapSet(); + apiBeatmapSet.OnlineID = requested_beatmap_set_id; + apiBeatmapSet.Beatmaps = apiBeatmapSet.Beatmaps.Append(new APIBeatmap + { + DifficultyName = "Target difficulty", + OnlineID = requested_beatmap_id, + }).ToArray(); + + gbr.TriggerSuccess(apiBeatmapSet); + return true; + } + + return false; + }; + }); [Test] public void TestBeatmapLink() { AddUntilStep("Beatmap overlay displayed", () => Game.ChildrenOfType().FirstOrDefault()?.State.Value == Visibility.Visible); + AddUntilStep("Beatmap overlay showing content", () => Game.ChildrenOfType().FirstOrDefault()?.Beatmap.Value.OnlineID == requested_beatmap_id); } } } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs index 1339c514e4..1aa56896d3 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs @@ -5,18 +5,48 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; namespace osu.Game.Tests.Visual.Navigation { public class TestSceneStartupBeatmapSetDisplay : OsuGameTestScene { - protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { "osu://s/1" }); + private const int requested_beatmap_set_id = 1; + + protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { $"osu://s/{requested_beatmap_set_id}" }); + + [SetUp] + public void Setup() => Schedule(() => + { + ((DummyAPIAccess)API).HandleRequest = request => + { + switch (request) + { + case GetBeatmapSetRequest gbr: + + var apiBeatmapSet = CreateAPIBeatmapSet(); + apiBeatmapSet.OnlineID = requested_beatmap_set_id; + apiBeatmapSet.Beatmaps = apiBeatmapSet.Beatmaps.Append(new APIBeatmap + { + DifficultyName = "Target difficulty", + OnlineID = 75, + }).ToArray(); + gbr.TriggerSuccess(apiBeatmapSet); + return true; + } + + return false; + }; + }); [Test] public void TestBeatmapSetLink() { AddUntilStep("Beatmap overlay displayed", () => Game.ChildrenOfType().FirstOrDefault()?.State.Value == Visibility.Visible); + AddUntilStep("Beatmap overlay showing content", () => Game.ChildrenOfType().FirstOrDefault()?.Header.BeatmapSet.Value.OnlineID == requested_beatmap_set_id); } } } From 6de4e05e492aa7ff03e068000ffdf83cb2dd65d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 17:14:00 +0900 Subject: [PATCH 14/19] Fix current selection not being correctly maintained when `BeatmapPicker` updates its display --- osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index 59e8e8db3c..031442814d 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -183,7 +183,14 @@ namespace osu.Game.Overlays.BeatmapSet } starRatingContainer.FadeOut(100); - Beatmap.Value = Difficulties.FirstOrDefault()?.Beatmap; + + // If a selection is already made, try and maintain it. + if (Beatmap.Value != null) + Beatmap.Value = Difficulties.FirstOrDefault(b => b.Beatmap.OnlineID == Beatmap.Value.OnlineID)?.Beatmap; + + // Else just choose the first available difficulty for now. + Beatmap.Value ??= Difficulties.FirstOrDefault()?.Beatmap; + plays.Value = BeatmapSet?.PlayCount ?? 0; favourites.Value = BeatmapSet?.FavouriteCount ?? 0; From d1d6847d3205d5ea1226dc063d7315b95cd47491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Feb 2022 22:24:37 +0100 Subject: [PATCH 15/19] Add comment about split usage in osu:// protocol link handling --- osu.Game/OsuGame.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 390e96d768..77eec004c0 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -921,6 +921,9 @@ namespace osu.Game if (!url.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) throw new ArgumentException("Invalid osu URL provided.", nameof(url)); + // note that `StringSplitOptions.RemoveEmptyEntries` is not explicitly specified here + // in order to ensure that the protocol URL is valid (i.e. it has two slashes in the `osu://` part, + // causing the link content to be stored to the 2nd index rather than the 1st). string[] pieces = url.Split('/'); switch (pieces[2]) From 87da650dfb5651c608562865a37db35a0e04c32e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Feb 2022 13:51:25 +0900 Subject: [PATCH 16/19] Update framework --- osu.Android.props | 2 +- osu.Game/Tests/Visual/SkinnableTestScene.cs | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 24a0d20874..526ce959a6 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index cd675e467b..1107089a46 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load() { - var dllStore = new DllResourceStore(DynamicCompilationOriginal.GetType().Assembly); + var dllStore = new DllResourceStore(GetType().Assembly); metricsSkin = new TestLegacySkin(new SkinInfo { Name = "metrics-skin" }, new NamespacedResourceStore(dllStore, "Resources/metrics_skin"), this, true); defaultSkin = new DefaultLegacySkin(this); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4c6f81defa..7dfd099df1 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 99b9de3fe2..9d0e1790f0 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From a6b6644c2e441513bfba493fbe56e307996bd4ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Feb 2022 16:22:13 +0900 Subject: [PATCH 17/19] Replace LINQ queries with recommendations --- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index e61720d8b9..70b7d1f740 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -96,12 +96,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } // we should only be checking the bonus/progress after the spinner has fully completed. - if (!results.OfType().Any(r => r.TimeCompleted != null)) + if (results.OfType().All(r => r.TimeCompleted == null)) return false; return results.Any(r => r.Type == HitResult.SmallBonus) - && !results.Any(r => r.Type == HitResult.LargeBonus); + && results.All(r => r.Type != HitResult.LargeBonus); } }); } From 054ed546e3800e6aa0425b362b68dacf9e19fb75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Feb 2022 16:56:50 +0900 Subject: [PATCH 18/19] Fix intermittent failures in remaining test method --- .../Mods/TestSceneOsuModSpunOut.cs | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index 70b7d1f740..a8953c1a6f 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -26,13 +26,37 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods protected override bool AllowFail => true; [Test] - public void TestSpinnerAutoCompleted() => CreateModTest(new ModTestData + public void TestSpinnerAutoCompleted() { - Mod = new OsuModSpunOut(), - Autoplay = false, - Beatmap = singleSpinnerBeatmap, - PassCondition = () => Player.ChildrenOfType().SingleOrDefault()?.Progress >= 1 - }); + DrawableSpinner spinner = null; + JudgementResult lastResult = null; + + CreateModTest(new ModTestData + { + Mod = new OsuModSpunOut(), + Autoplay = false, + Beatmap = singleSpinnerBeatmap, + PassCondition = () => + { + // Bind to the first spinner's results for further tracking. + if (spinner == null) + { + // We only care about the first spinner we encounter for this test. + var nextSpinner = Player.ChildrenOfType().SingleOrDefault(); + + if (nextSpinner == null) + return false; + + lastResult = null; + + spinner = nextSpinner; + spinner.OnNewResult += (o, result) => lastResult = result; + } + + return lastResult?.Type == HitResult.Great; + } + }); + } [TestCase(null)] [TestCase(typeof(OsuModDoubleTime))] From 5d73691de4e13ed827d0a731ea909e7b33d115d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Feb 2022 17:02:39 +0900 Subject: [PATCH 19/19] Use existing `HandleLink` flow rather than reimplmenting --- osu.Game/OsuGame.cs | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 77eec004c0..fa5a336b7c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -906,7 +906,7 @@ namespace osu.Game if (firstPath.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) { - handleOsuProtocolUrl(firstPath); + HandleLink(firstPath); } else { @@ -916,30 +916,6 @@ namespace osu.Game } } - private void handleOsuProtocolUrl(string url) - { - if (!url.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) - throw new ArgumentException("Invalid osu URL provided.", nameof(url)); - - // note that `StringSplitOptions.RemoveEmptyEntries` is not explicitly specified here - // in order to ensure that the protocol URL is valid (i.e. it has two slashes in the `osu://` part, - // causing the link content to be stored to the 2nd index rather than the 1st). - string[] pieces = url.Split('/'); - - switch (pieces[2]) - { - case "s": - if (int.TryParse(pieces[3], out int beatmapSetId)) - ShowBeatmapSet(beatmapSetId); - break; - - case "b": - if (int.TryParse(pieces[3], out int beatmapId)) - ShowBeatmap(beatmapId); - break; - } - } - private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays) { otherOverlays.Where(o => o != overlay).ForEach(o => o.Hide());