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.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
index 24e69703a6..a8953c1a6f 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
@@ -8,12 +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.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
@@ -23,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))]
@@ -48,7 +75,57 @@ 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;
+
+ float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration);
+
+ return Precision.AlmostEquals(counter.Result.Value, rotationSpeed * 1000 * 60, 1);
+ }
+ });
+ }
+
+ [Test]
+ public void TestSpinnerGetsNoBonusScore()
+ {
+ DrawableSpinner spinner = null;
+ List results = new List();
+
+ 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;
+
+ spinner = nextSpinner;
+ spinner.OnNewResult += (o, result) => results.Add(result);
+
+ results.Clear();
+ }
+
+ // we should only be checking the bonus/progress after the spinner has fully completed.
+ if (results.OfType().All(r => r.TimeCompleted == null))
+ return false;
+
+ return
+ results.Any(r => r.Type == HitResult.SmallBonus)
+ && results.All(r => r.Type != HitResult.LargeBonus);
}
});
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
index 098c639949..9be0dc748a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
@@ -45,7 +45,11 @@ 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));
+
+ // 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));
}
}
}
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/Navigation/TestSceneStartupBeatmapDisplay.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs
new file mode 100644
index 0000000000..961b7dedc3
--- /dev/null
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.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 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
+ {
+ 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
new file mode 100644
index 0000000000..1aa56896d3
--- /dev/null
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.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.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
+ {
+ 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);
+ }
+ }
+}
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 3517777325..b18daea453 100644
--- a/osu.Game/Online/Chat/MessageFormatter.cs
+++ b/osu.Game/Online/Chat/MessageFormatter.cs
@@ -268,10 +268,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/OsuGame.cs b/osu.Game/OsuGame.cs
index 5b58dec0c3..fa5a336b7c 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,20 @@ 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))
+ {
+ HandleLink(firstPath);
+ }
+ else
+ {
+ Task.Run(() => Import(paths));
+ }
+ }
}
}
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;
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;
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 @@
-
+