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/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/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
index adaa24d542..d1c1558003 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
@@ -49,6 +49,8 @@ namespace osu.Game.Tests.Visual.Editing
double originalTimelineZoom = 0;
double changedTimelineZoom = 0;
+ AddUntilStep("wait for timeline load", () => Editor.ChildrenOfType().SingleOrDefault()?.IsLoaded == true);
+
AddStep("Set beat divisor", () => Editor.Dependencies.Get().Value = 16);
AddStep("Set timeline zoom", () =>
{
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.Tests/Visual/UserInterface/TestSceneModSwitchSmall.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchSmall.cs
new file mode 100644
index 0000000000..447352b7a6
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchSmall.cs
@@ -0,0 +1,85 @@
+// 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 NUnit.Framework;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Overlays;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Catch;
+using osu.Game.Rulesets.Mania;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Taiko;
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ [TestFixture]
+ public class TestSceneModSwitchSmall : OsuTestScene
+ {
+ [Test]
+ public void TestOsu() => createSwitchTestFor(new OsuRuleset());
+
+ [Test]
+ public void TestTaiko() => createSwitchTestFor(new TaikoRuleset());
+
+ [Test]
+ public void TestCatch() => createSwitchTestFor(new CatchRuleset());
+
+ [Test]
+ public void TestMania() => createSwitchTestFor(new ManiaRuleset());
+
+ private void createSwitchTestFor(Ruleset ruleset)
+ {
+ AddStep("no colour scheme", () => Child = createContent(ruleset, null));
+
+ foreach (var scheme in Enum.GetValues(typeof(OverlayColourScheme)).Cast())
+ {
+ AddStep($"{scheme} colour scheme", () => Child = createContent(ruleset, scheme));
+ }
+
+ AddToggleStep("toggle active", active => this.ChildrenOfType().ForEach(s => s.Active.Value = active));
+ }
+
+ private static Drawable createContent(Ruleset ruleset, OverlayColourScheme? colourScheme)
+ {
+ var switchFlow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(10),
+ Padding = new MarginPadding(20),
+ ChildrenEnumerable = ruleset.CreateAllMods()
+ .GroupBy(mod => mod.Type)
+ .Select(group => new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Full,
+ Spacing = new Vector2(5),
+ ChildrenEnumerable = group.Select(mod => new ModSwitchSmall(mod))
+ })
+ };
+
+ if (colourScheme != null)
+ {
+ return new DependencyProvidingContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ CachedDependencies = new (Type, object)[]
+ {
+ (typeof(OverlayColourProvider), new OverlayColourProvider(colourScheme.Value))
+ },
+ Child = switchFlow
+ };
+ }
+
+ return switchFlow;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs
new file mode 100644
index 0000000000..dbde7ce425
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs
@@ -0,0 +1,85 @@
+// 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 NUnit.Framework;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Overlays;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Catch;
+using osu.Game.Rulesets.Mania;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Taiko;
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ [TestFixture]
+ public class TestSceneModSwitchTiny : OsuTestScene
+ {
+ [Test]
+ public void TestOsu() => createSwitchTestFor(new OsuRuleset());
+
+ [Test]
+ public void TestTaiko() => createSwitchTestFor(new TaikoRuleset());
+
+ [Test]
+ public void TestCatch() => createSwitchTestFor(new CatchRuleset());
+
+ [Test]
+ public void TestMania() => createSwitchTestFor(new ManiaRuleset());
+
+ private void createSwitchTestFor(Ruleset ruleset)
+ {
+ AddStep("no colour scheme", () => Child = createContent(ruleset, null));
+
+ foreach (var scheme in Enum.GetValues(typeof(OverlayColourScheme)).Cast())
+ {
+ AddStep($"{scheme} colour scheme", () => Child = createContent(ruleset, scheme));
+ }
+
+ AddToggleStep("toggle active", active => this.ChildrenOfType().ForEach(s => s.Active.Value = active));
+ }
+
+ private static Drawable createContent(Ruleset ruleset, OverlayColourScheme? colourScheme)
+ {
+ var switchFlow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(10),
+ Padding = new MarginPadding(20),
+ ChildrenEnumerable = ruleset.CreateAllMods()
+ .GroupBy(mod => mod.Type)
+ .Select(group => new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Full,
+ Spacing = new Vector2(5),
+ ChildrenEnumerable = group.Select(mod => new ModSwitchTiny(mod))
+ })
+ };
+
+ if (colourScheme != null)
+ {
+ return new DependencyProvidingContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ CachedDependencies = new (Type, object)[]
+ {
+ (typeof(OverlayColourProvider), new OverlayColourProvider(colourScheme.Value))
+ },
+ Child = switchFlow
+ };
+ }
+
+ return switchFlow;
+ }
+ }
+}
diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs
index 886ba7ef92..afedf36cad 100644
--- a/osu.Game/Graphics/OsuColour.cs
+++ b/osu.Game/Graphics/OsuColour.cs
@@ -5,6 +5,7 @@ using System;
using osu.Framework.Extensions.Color4Extensions;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Utils;
@@ -157,6 +158,36 @@ namespace osu.Game.Graphics
}
}
+ ///
+ /// Retrieves the main accent colour for a .
+ ///
+ public Color4 ForModType(ModType modType)
+ {
+ switch (modType)
+ {
+ case ModType.Automation:
+ return Blue1;
+
+ case ModType.DifficultyIncrease:
+ return Red1;
+
+ case ModType.DifficultyReduction:
+ return Lime1;
+
+ case ModType.Conversion:
+ return Purple1;
+
+ case ModType.Fun:
+ return Pink1;
+
+ case ModType.System:
+ return Gray7;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(modType), modType, "Unknown mod type");
+ }
+ }
+
///
/// Returns a foreground text colour that is supposed to contrast well with
/// the supplied .
diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs
index d7974004b1..b18daea453 100644
--- a/osu.Game/Online/Chat/MessageFormatter.cs
+++ b/osu.Game/Online/Chat/MessageFormatter.cs
@@ -236,8 +236,7 @@ namespace osu.Game.Online.Chat
break;
default:
- linkType = LinkAction.External;
- break;
+ return new LinkDetails(LinkAction.External, url);
}
return new LinkDetails(linkType, args[2]);
@@ -269,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/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
index 157753c09f..0f87f04270 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
@@ -67,6 +67,8 @@ namespace osu.Game.Overlays.BeatmapListing
[Resolved]
private IAPIProvider api { get; set; }
+ private IBindable apiUser;
+
public BeatmapListingFilterControl()
{
RelativeSizeAxes = Axes.X;
@@ -127,7 +129,7 @@ namespace osu.Game.Overlays.BeatmapListing
}
[BackgroundDependencyLoader]
- private void load(OverlayColourProvider colourProvider)
+ private void load(OverlayColourProvider colourProvider, IAPIProvider api)
{
sortControlBackground.Colour = colourProvider.Background4;
}
@@ -161,6 +163,9 @@ namespace osu.Game.Overlays.BeatmapListing
sortCriteria.BindValueChanged(_ => queueUpdateSearch());
sortDirection.BindValueChanged(_ => queueUpdateSearch());
+
+ apiUser = api.LocalUser.GetBoundCopy();
+ apiUser.BindValueChanged(_ => queueUpdateSearch());
}
public void TakeFocus() => searchControl.TakeFocus();
@@ -190,6 +195,9 @@ namespace osu.Game.Overlays.BeatmapListing
resetSearch();
+ if (!api.IsLoggedIn)
+ return;
+
queryChangedDebounce = Scheduler.AddDelayed(() =>
{
resetSearch();
diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs
index fbed234cc7..3476968ded 100644
--- a/osu.Game/Overlays/BeatmapListingOverlay.cs
+++ b/osu.Game/Overlays/BeatmapListingOverlay.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Localisation;
using osu.Framework.Graphics;
@@ -19,6 +20,7 @@ using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.Containers;
+using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Resources.Localisation.Web;
@@ -32,6 +34,11 @@ namespace osu.Game.Overlays
[Resolved]
private PreviewTrackManager previewTrackManager { get; set; }
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ private IBindable apiUser;
+
private Drawable currentContent;
private Container panelTarget;
private FillFlowContainer foundContent;
@@ -93,6 +100,13 @@ namespace osu.Game.Overlays
{
base.LoadComplete();
filterControl.CardSize.BindValueChanged(_ => onCardSizeChanged());
+
+ apiUser = api.LocalUser.GetBoundCopy();
+ apiUser.BindValueChanged(_ =>
+ {
+ if (api.IsLoggedIn)
+ addContentToResultsArea(Drawable.Empty());
+ });
}
public void ShowWithSearch(string query)
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/Rulesets/UI/ModSwitchSmall.cs b/osu.Game/Rulesets/UI/ModSwitchSmall.cs
new file mode 100644
index 0000000000..676bbac95c
--- /dev/null
+++ b/osu.Game/Rulesets/UI/ModSwitchSmall.cs
@@ -0,0 +1,109 @@
+// 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.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Overlays;
+using osu.Game.Rulesets.Mods;
+using osuTK;
+using osuTK.Graphics;
+
+#nullable enable
+
+namespace osu.Game.Rulesets.UI
+{
+ public class ModSwitchSmall : CompositeDrawable
+ {
+ public BindableBool Active { get; } = new BindableBool();
+
+ public const float DEFAULT_SIZE = 60;
+
+ private readonly IMod mod;
+
+ private readonly SpriteIcon background;
+ private readonly SpriteIcon? modIcon;
+
+ private Color4 activeForegroundColour;
+ private Color4 inactiveForegroundColour;
+
+ private Color4 activeBackgroundColour;
+ private Color4 inactiveBackgroundColour;
+
+ public ModSwitchSmall(IMod mod)
+ {
+ this.mod = mod;
+
+ AutoSizeAxes = Axes.Both;
+
+ FillFlowContainer contentFlow;
+ ModSwitchTiny tinySwitch;
+
+ InternalChildren = new Drawable[]
+ {
+ background = new SpriteIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(DEFAULT_SIZE),
+ Icon = OsuIcon.ModBg
+ },
+ contentFlow = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Spacing = new Vector2(0, 4),
+ Direction = FillDirection.Vertical,
+ Child = tinySwitch = new ModSwitchTiny(mod)
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Scale = new Vector2(0.6f),
+ Active = { BindTarget = Active }
+ }
+ }
+ };
+
+ if (mod.Icon != null)
+ {
+ contentFlow.Insert(-1, modIcon = new SpriteIcon
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Size = new Vector2(21),
+ Icon = mod.Icon.Value
+ });
+ tinySwitch.Scale = new Vector2(0.3f);
+ }
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OsuColour colours, OverlayColourProvider? colourProvider)
+ {
+ inactiveForegroundColour = colourProvider?.Background5 ?? colours.Gray3;
+ activeForegroundColour = colours.ForModType(mod.Type);
+
+ inactiveBackgroundColour = colourProvider?.Background2 ?? colours.Gray5;
+ activeBackgroundColour = Interpolation.ValueAt(0.1f, Colour4.Black, activeForegroundColour, 0, 1);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Active.BindValueChanged(_ => updateState(), true);
+ FinishTransforms(true);
+ }
+
+ private void updateState()
+ {
+ modIcon?.FadeColour(Active.Value ? activeForegroundColour : inactiveForegroundColour, 200, Easing.OutQuint);
+ background.FadeColour(Active.Value ? activeBackgroundColour : inactiveBackgroundColour, 200, Easing.OutQuint);
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/UI/ModSwitchTiny.cs b/osu.Game/Rulesets/UI/ModSwitchTiny.cs
new file mode 100644
index 0000000000..b1d453f588
--- /dev/null
+++ b/osu.Game/Rulesets/UI/ModSwitchTiny.cs
@@ -0,0 +1,93 @@
+// 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.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Overlays;
+using osu.Game.Rulesets.Mods;
+using osuTK;
+using osuTK.Graphics;
+
+#nullable enable
+
+namespace osu.Game.Rulesets.UI
+{
+ public class ModSwitchTiny : CompositeDrawable
+ {
+ public BindableBool Active { get; } = new BindableBool();
+
+ public const float DEFAULT_HEIGHT = 30;
+
+ private readonly IMod mod;
+
+ private readonly Box background;
+ private readonly OsuSpriteText acronymText;
+
+ private Color4 activeForegroundColour;
+ private Color4 inactiveForegroundColour;
+
+ private Color4 activeBackgroundColour;
+ private Color4 inactiveBackgroundColour;
+
+ public ModSwitchTiny(IMod mod)
+ {
+ this.mod = mod;
+ Size = new Vector2(73, DEFAULT_HEIGHT);
+
+ InternalChild = new CircularContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Children = new Drawable[]
+ {
+ background = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ acronymText = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Shadow = false,
+ Font = OsuFont.Numeric.With(size: 24, weight: FontWeight.Black),
+ Text = mod.Acronym,
+ Margin = new MarginPadding
+ {
+ Top = 4
+ }
+ }
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OsuColour colours, OverlayColourProvider? colourProvider)
+ {
+ inactiveBackgroundColour = colourProvider?.Background5 ?? colours.Gray3;
+ activeBackgroundColour = colours.ForModType(mod.Type);
+
+ inactiveForegroundColour = colourProvider?.Background2 ?? colours.Gray5;
+ activeForegroundColour = Interpolation.ValueAt(0.1f, Colour4.Black, activeForegroundColour, 0, 1);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Active.BindValueChanged(_ => updateState(), true);
+ FinishTransforms(true);
+ }
+
+ private void updateState()
+ {
+ acronymText.FadeColour(Active.Value ? activeForegroundColour : inactiveForegroundColour, 200, Easing.OutQuint);
+ background.FadeColour(Active.Value ? activeBackgroundColour : inactiveBackgroundColour, 200, Easing.OutQuint);
+ }
+ }
+}
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 @@
-
+