diff --git a/Directory.Build.props b/Directory.Build.props
index 5bdf12218c..709545bf1d 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -26,14 +26,6 @@
true
$(NoWarn);CS1591
-
-
- $(NoWarn);NU1701
-
false
ppy Pty Ltd
@@ -42,7 +34,7 @@
https://github.com/ppy/osu
Automated release.
ppy Pty Ltd
- Copyright (c) 2021 ppy Pty Ltd
+ Copyright (c) 2022 ppy Pty Ltd
osu game
diff --git a/LICENCE b/LICENCE
index b5962ad3b2..d3e7537cef 100644
--- a/LICENCE
+++ b/LICENCE
@@ -1,4 +1,4 @@
-Copyright (c) 2021 ppy Pty Ltd .
+Copyright (c) 2022 ppy Pty Ltd .
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Templates/osu.Game.Templates.csproj b/Templates/osu.Game.Templates.csproj
index 4624d3d771..b8c3ad373a 100644
--- a/Templates/osu.Game.Templates.csproj
+++ b/Templates/osu.Game.Templates.csproj
@@ -8,7 +8,7 @@
https://github.com/ppy/osu/blob/master/Templates
https://github.com/ppy/osu
Automated release.
- Copyright (c) 2021 ppy Pty Ltd
+ Copyright (c) 2022 ppy Pty Ltd
Templates to use when creating a ruleset for consumption in osu!.
dotnet-new;templates;osu
netstandard2.1
diff --git a/osu.Android.props b/osu.Android.props
index fbe13b11ee..10685d5990 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index a06484214b..3ca6411812 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -25,7 +25,6 @@
-
@@ -33,7 +32,6 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec
index 1757fd7c73..dc1ec17e2c 100644
--- a/osu.Desktop/osu.nuspec
+++ b/osu.Desktop/osu.nuspec
@@ -11,7 +11,7 @@
false
A free-to-win rhythm game. Rhythm is just a *click* away!
testing
- Copyright (c) 2021 ppy Pty Ltd
+ Copyright (c) 2022 ppy Pty Ltd
en-AU
diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
index 7516e7500b..76c49edf78 100644
--- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
+++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
@@ -69,6 +69,34 @@ namespace osu.Game.Tests.NonVisual.Skinning
"Gameplay/osu/followpoint",
"followpoint", 1
},
+ new object[]
+ {
+ // Looking up a filename with extension specified should work.
+ new[] { "followpoint.png" },
+ "followpoint.png",
+ "followpoint.png", 1
+ },
+ new object[]
+ {
+ // Looking up a filename with extension specified should also work with @2x sprites.
+ new[] { "followpoint@2x.png" },
+ "followpoint.png",
+ "followpoint@2x.png", 2
+ },
+ new object[]
+ {
+ // Looking up a path with extension specified should work.
+ new[] { "Gameplay/osu/followpoint.png" },
+ "Gameplay/osu/followpoint.png",
+ "Gameplay/osu/followpoint.png", 1
+ },
+ new object[]
+ {
+ // Looking up a path with extension specified should also work with @2x sprites.
+ new[] { "Gameplay/osu/followpoint@2x.png" },
+ "Gameplay/osu/followpoint.png",
+ "Gameplay/osu/followpoint@2x.png", 2
+ },
};
[TestCaseSource(nameof(fallbackTestCases))]
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
index 8b7e1c4e58..5d25287e45 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
@@ -150,10 +150,12 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("import score", () => imported = scoreManager.Import(getScoreInfo(true)));
AddUntilStep("state is available", () => downloadButton.State.Value == DownloadState.LocallyAvailable);
+ AddAssert("button is enabled", () => downloadButton.ChildrenOfType().First().Enabled.Value);
AddStep("delete score", () => scoreManager.Delete(imported.Value));
AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
+ AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value);
}
[Test]
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index 381b9b58bd..714951cc42 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -8,13 +8,12 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
-using osu.Framework.Extensions;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
-using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Database;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
@@ -27,6 +26,7 @@ using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Select;
+using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -35,10 +35,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
private BeatmapManager manager;
private RulesetStore rulesets;
- private List beatmaps;
+ private IList beatmaps => importedBeatmapSet?.PerformRead(s => s.Beatmaps) ?? new List();
private TestMultiplayerMatchSongSelect songSelect;
+ private Live importedBeatmapSet;
+
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
@@ -46,44 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
- beatmaps = new List();
-
- var metadata = new BeatmapMetadata
- {
- Artist = "Some Artist",
- Title = "Some Beatmap",
- Author = { Username = "Some Author" },
- };
-
- var beatmapSetInfo = new BeatmapSetInfo
- {
- OnlineID = 10,
- Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
- DateAdded = DateTimeOffset.UtcNow
- };
-
- for (int i = 0; i < 8; ++i)
- {
- int beatmapId = 10 * 10 + i;
-
- int length = RNG.Next(30000, 200000);
- double bpm = RNG.NextSingle(80, 200);
-
- var beatmap = new BeatmapInfo
- {
- Ruleset = rulesets.GetRuleset(i % 4) ?? throw new InvalidOperationException(),
- OnlineID = beatmapId,
- Length = length,
- BPM = bpm,
- Metadata = metadata,
- Difficulty = new BeatmapDifficulty()
- };
-
- beatmaps.Add(beatmap);
- beatmapSetInfo.Beatmaps.Add(beatmap);
- }
-
- manager.Import(beatmapSetInfo);
+ importedBeatmapSet = manager.Import(TestResources.CreateTestBeatmapSetInfo(8, rulesets.AvailableRulesets.ToArray()));
}
public override void SetUpSteps()
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
index 13404a9810..72dfc53c01 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.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 NUnit.Framework;
@@ -17,7 +16,6 @@ using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
-using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
@@ -60,20 +58,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Anchor = Anchor.Centre,
Size = new Vector2(550f, 450f),
Scope = BeatmapLeaderboardScope.Local,
- BeatmapInfo = new BeatmapInfo
- {
- ID = Guid.NewGuid(),
- Metadata = new BeatmapMetadata
- {
- Title = "TestSong",
- Artist = "TestArtist",
- Author = new RealmUser
- {
- Username = "TestAuthor"
- },
- },
- DifficultyName = "Insane"
- },
+ BeatmapInfo = TestResources.CreateTestBeatmapSetInfo().Beatmaps.First()
}
},
dialogOverlay = new DialogOverlay()
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs
new file mode 100644
index 0000000000..34d9ddcc4e
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs
@@ -0,0 +1,140 @@
+// 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.Framework.Utils;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays.Mods;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Mods;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ [TestFixture]
+ public class TestSceneModSelectScreen : OsuManualInputManagerTestScene
+ {
+ [Resolved]
+ private RulesetStore rulesetStore { get; set; }
+
+ private ModSelectScreen modSelectScreen;
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("clear contents", Clear);
+ AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0));
+ AddStep("reset mods", () => SelectedMods.SetDefault());
+ }
+
+ private void createScreen()
+ {
+ AddStep("create screen", () => Child = modSelectScreen = new ModSelectScreen
+ {
+ RelativeSizeAxes = Axes.Both,
+ State = { Value = Visibility.Visible },
+ SelectedMods = { BindTarget = SelectedMods }
+ });
+ waitForColumnLoad();
+ }
+
+ [Test]
+ public void TestStateChange()
+ {
+ createScreen();
+ AddStep("toggle state", () => modSelectScreen.ToggleVisibility());
+ }
+
+ [Test]
+ public void TestPreexistingSelection()
+ {
+ AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModAlternate(), new OsuModDaycore() });
+ createScreen();
+ AddUntilStep("two panels active", () => modSelectScreen.ChildrenOfType().Count(panel => panel.Active.Value) == 2);
+ AddAssert("mod multiplier correct", () =>
+ {
+ double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
+ return Precision.AlmostEquals(multiplier, modSelectScreen.ChildrenOfType().Single().Current.Value);
+ });
+ assertCustomisationToggleState(disabled: false, active: false);
+ }
+
+ [Test]
+ public void TestExternalSelection()
+ {
+ createScreen();
+ AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModAlternate(), new OsuModDaycore() });
+ AddUntilStep("two panels active", () => modSelectScreen.ChildrenOfType().Count(panel => panel.Active.Value) == 2);
+ AddAssert("mod multiplier correct", () =>
+ {
+ double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
+ return Precision.AlmostEquals(multiplier, modSelectScreen.ChildrenOfType().Single().Current.Value);
+ });
+ assertCustomisationToggleState(disabled: false, active: false);
+ }
+
+ [Test]
+ public void TestRulesetChange()
+ {
+ createScreen();
+ changeRuleset(0);
+ changeRuleset(1);
+ changeRuleset(2);
+ changeRuleset(3);
+ }
+
+ [Test]
+ public void TestCustomisationToggleState()
+ {
+ createScreen();
+ assertCustomisationToggleState(disabled: true, active: false);
+
+ AddStep("select customisable mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });
+ assertCustomisationToggleState(disabled: false, active: false);
+
+ AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
+ assertCustomisationToggleState(disabled: false, active: true);
+
+ AddStep("dismiss mod customisation", () =>
+ {
+ InputManager.MoveMouseTo(modSelectScreen.ChildrenOfType().Single());
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddStep("append another mod not requiring config", () => SelectedMods.Value = SelectedMods.Value.Append(new OsuModFlashlight()).ToArray());
+ assertCustomisationToggleState(disabled: false, active: false);
+
+ AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() });
+ assertCustomisationToggleState(disabled: true, active: false);
+
+ AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
+ assertCustomisationToggleState(disabled: false, active: true);
+
+ AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() });
+ assertCustomisationToggleState(disabled: true, active: false); // config was dismissed without explicit user action.
+ }
+
+ private void waitForColumnLoad() => AddUntilStep("all column content loaded",
+ () => modSelectScreen.ChildrenOfType().Any() && modSelectScreen.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded));
+
+ private void changeRuleset(int id)
+ {
+ AddStep($"set ruleset to {id}", () => Ruleset.Value = rulesetStore.GetRuleset(id));
+ waitForColumnLoad();
+ }
+
+ private void assertCustomisationToggleState(bool disabled, bool active)
+ {
+ ShearedToggleButton getToggle() => modSelectScreen.ChildrenOfType().Single();
+
+ AddAssert($"customisation toggle is {(disabled ? "" : "not ")}disabled", () => getToggle().Active.Disabled == disabled);
+ AddAssert($"customisation toggle is {(active ? "" : "not ")}active", () => getToggle().Active.Value == active);
+ }
+ }
+}
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 397d47c389..bb64ec796c 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -17,7 +17,6 @@ using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
using osu.Game.Storyboards;
@@ -152,24 +151,7 @@ namespace osu.Game.Beatmaps
{
const double excess_length = 1000;
- var lastObject = Beatmap?.HitObjects.LastOrDefault();
-
- double length;
-
- switch (lastObject)
- {
- case null:
- length = emptyLength;
- break;
-
- case IHasDuration endTime:
- length = endTime.EndTime + excess_length;
- break;
-
- default:
- length = lastObject.StartTime + excess_length;
- break;
- }
+ double length = (BeatmapInfo?.Length + excess_length) ?? emptyLength;
return audioManager.Tracks.GetVirtual(length);
}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs
new file mode 100644
index 0000000000..4729765084
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs
@@ -0,0 +1,44 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+using System;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.SignalR;
+using osu.Framework.Logging;
+
+namespace osu.Game.Online.Multiplayer
+{
+ public static class MultiplayerClientExtensions
+ {
+ public static void FireAndForget(this Task task, Action? onSuccess = null, Action? onError = null) =>
+ task.ContinueWith(t =>
+ {
+ if (t.IsFaulted)
+ {
+ Exception? exception = t.Exception;
+
+ if (exception is AggregateException ae)
+ exception = ae.InnerException;
+
+ Debug.Assert(exception != null);
+
+ string message = exception is HubException
+ // HubExceptions arrive with additional message context added, but we want to display the human readable message:
+ // "An unexpected error occurred invoking 'AddPlaylistItem' on the server.InvalidStateException: Can't enqueue more than 3 items at once."
+ // We generally use the message field for a user-parseable error (eventually to be replaced), so drop the first part for now.
+ ? exception.Message.Substring(exception.Message.IndexOf(':') + 1).Trim()
+ : exception.Message;
+
+ Logger.Log(message, level: LogLevel.Important);
+ onError?.Invoke(exception);
+ }
+ else
+ {
+ onSuccess?.Invoke();
+ }
+ });
+ }
+}
diff --git a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs
index 4fc3a904fa..248d4f288e 100644
--- a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs
+++ b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs
@@ -20,6 +20,8 @@ namespace osu.Game.Overlays.Mods
{
public class DifficultyMultiplierDisplay : CompositeDrawable, IHasCurrentValue
{
+ public const float HEIGHT = 42;
+
public Bindable Current
{
get => current.Current;
@@ -42,13 +44,12 @@ namespace osu.Game.Overlays.Mods
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
- private const float height = 42;
private const float multiplier_value_area_width = 56;
private const float transition_duration = 200;
public DifficultyMultiplierDisplay()
{
- Height = height;
+ Height = HEIGHT;
AutoSizeAxes = Axes.X;
InternalChild = new Container
@@ -145,8 +146,9 @@ namespace osu.Game.Overlays.Mods
protected override void LoadComplete()
{
base.LoadComplete();
+
current.BindValueChanged(_ => updateState(), true);
- FinishTransforms(true);
+
// required to prevent the counter initially rolling up from 0 to 1
// due to `Current.Value` having a nonstandard default value of 1.
multiplierCounter.SetCountWithoutRolling(Current.Value);
diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs
index 736a0205e2..f84ae4ac8a 100644
--- a/osu.Game/Overlays/Mods/ModColumn.cs
+++ b/osu.Game/Overlays/Mods/ModColumn.cs
@@ -31,6 +31,10 @@ namespace osu.Game.Overlays.Mods
{
public class ModColumn : CompositeDrawable
{
+ public readonly Container TopLevelContent;
+
+ public readonly ModType ModType;
+
private Func? filter;
///
@@ -48,7 +52,8 @@ namespace osu.Game.Overlays.Mods
}
}
- private readonly ModType modType;
+ public Bindable> SelectedMods = new Bindable>(Array.Empty());
+
private readonly Key[]? toggleKeys;
private readonly Bindable>> availableMods = new Bindable>>();
@@ -69,95 +74,103 @@ namespace osu.Game.Overlays.Mods
public ModColumn(ModType modType, bool allowBulkSelection, Key[]? toggleKeys = null)
{
- this.modType = modType;
+ ModType = modType;
this.toggleKeys = toggleKeys;
Width = 320;
RelativeSizeAxes = Axes.Y;
Shear = new Vector2(ModPanel.SHEAR_X, 0);
- CornerRadius = ModPanel.CORNER_RADIUS;
- Masking = true;
Container controlContainer;
InternalChildren = new Drawable[]
{
- new Container
- {
- RelativeSizeAxes = Axes.X,
- Height = header_height + ModPanel.CORNER_RADIUS,
- Children = new Drawable[]
- {
- headerBackground = new Box
- {
- RelativeSizeAxes = Axes.X,
- Height = header_height + ModPanel.CORNER_RADIUS
- },
- headerText = new OsuTextFlowContainer(t =>
- {
- t.Font = OsuFont.TorusAlternate.With(size: 17);
- t.Shadow = false;
- t.Colour = Colour4.Black;
- })
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Shear = new Vector2(-ModPanel.SHEAR_X, 0),
- Padding = new MarginPadding
- {
- Horizontal = 17,
- Bottom = ModPanel.CORNER_RADIUS
- }
- }
- }
- },
- new Container
+ TopLevelContent = new Container
{
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Top = header_height },
- Child = contentContainer = new Container
+ CornerRadius = ModPanel.CORNER_RADIUS,
+ Masking = true,
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- CornerRadius = ModPanel.CORNER_RADIUS,
- BorderThickness = 3,
- Children = new Drawable[]
+ new Container
{
- contentBackground = new Box
+ RelativeSizeAxes = Axes.X,
+ Height = header_height + ModPanel.CORNER_RADIUS,
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.Both
- },
- new GridContainer
+ headerBackground = new Box
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = header_height + ModPanel.CORNER_RADIUS
+ },
+ headerText = new OsuTextFlowContainer(t =>
+ {
+ t.Font = OsuFont.TorusAlternate.With(size: 17);
+ t.Shadow = false;
+ t.Colour = Colour4.Black;
+ })
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Shear = new Vector2(-ModPanel.SHEAR_X, 0),
+ Padding = new MarginPadding
+ {
+ Horizontal = 17,
+ Bottom = ModPanel.CORNER_RADIUS
+ }
+ }
+ }
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Top = header_height },
+ Child = contentContainer = new Container
{
RelativeSizeAxes = Axes.Both,
- RowDimensions = new[]
+ Masking = true,
+ CornerRadius = ModPanel.CORNER_RADIUS,
+ BorderThickness = 3,
+ Children = new Drawable[]
{
- new Dimension(GridSizeMode.AutoSize),
- new Dimension()
- },
- Content = new[]
- {
- new Drawable[]
+ contentBackground = new Box
{
- controlContainer = new Container
- {
- RelativeSizeAxes = Axes.X,
- Padding = new MarginPadding { Horizontal = 14 }
- }
+ RelativeSizeAxes = Axes.Both
},
- new Drawable[]
+ new GridContainer
{
- new OsuScrollContainer
+ RelativeSizeAxes = Axes.Both,
+ RowDimensions = new[]
{
- RelativeSizeAxes = Axes.Both,
- ScrollbarOverlapsContent = false,
- Child = panelFlow = new FillFlowContainer
+ new Dimension(GridSizeMode.AutoSize),
+ new Dimension()
+ },
+ Content = new[]
+ {
+ new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Spacing = new Vector2(0, 7),
- Padding = new MarginPadding(7)
+ controlContainer = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Padding = new MarginPadding { Horizontal = 14 }
+ }
+ },
+ new Drawable[]
+ {
+ new NestedVerticalScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ ClampExtension = 100,
+ ScrollbarOverlapsContent = false,
+ Child = panelFlow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(0, 7),
+ Padding = new MarginPadding(7)
+ }
+ }
}
}
}
@@ -193,7 +206,7 @@ namespace osu.Game.Overlays.Mods
private void createHeaderText()
{
- IEnumerable headerTextWords = modType.Humanize(LetterCasing.Title).Split(' ');
+ IEnumerable headerTextWords = ModType.Humanize(LetterCasing.Title).Split(' ');
if (headerTextWords.Count() > 1)
{
@@ -209,7 +222,7 @@ namespace osu.Game.Overlays.Mods
{
availableMods.BindTo(game.AvailableMods);
- headerBackground.Colour = accentColour = colours.ForModType(modType);
+ headerBackground.Colour = accentColour = colours.ForModType(ModType);
if (toggleAllCheckbox != null)
{
@@ -225,6 +238,12 @@ namespace osu.Game.Overlays.Mods
{
base.LoadComplete();
availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods));
+ SelectedMods.BindValueChanged(_ =>
+ {
+ // if a load is in progress, don't try to update the selection - the load flow will do so.
+ if (latestLoadTask == null)
+ updateActiveState();
+ });
updateMods();
}
@@ -232,7 +251,7 @@ namespace osu.Game.Overlays.Mods
private void updateMods()
{
- var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(modType) ?? Array.Empty()).ToList();
+ var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(ModType) ?? Array.Empty()).ToList();
if (newMods.SequenceEqual(panelFlow.Children.Select(p => p.Mod)))
return;
@@ -250,11 +269,20 @@ namespace osu.Game.Overlays.Mods
{
panelFlow.ChildrenEnumerable = loaded;
- foreach (var panel in panelFlow)
- panel.Active.BindValueChanged(_ => updateToggleState());
- updateToggleState();
-
+ updateActiveState();
+ updateToggleAllState();
updateFilter();
+
+ foreach (var panel in panelFlow)
+ {
+ panel.Active.BindValueChanged(_ =>
+ {
+ updateToggleAllState();
+ SelectedMods.Value = panel.Active.Value
+ ? SelectedMods.Value.Append(panel.Mod).ToArray()
+ : SelectedMods.Value.Except(new[] { panel.Mod }).ToArray();
+ });
+ }
}, (cancellationTokenSource = new CancellationTokenSource()).Token);
loadTask.ContinueWith(_ =>
{
@@ -263,6 +291,12 @@ namespace osu.Game.Overlays.Mods
});
}
+ private void updateActiveState()
+ {
+ foreach (var panel in panelFlow)
+ panel.Active.Value = SelectedMods.Value.Contains(panel.Mod, EqualityComparer.Default);
+ }
+
#region Bulk select / deselect
private const double initial_multiple_selection_delay = 120;
@@ -297,7 +331,7 @@ namespace osu.Game.Overlays.Mods
}
}
- private void updateToggleState()
+ private void updateToggleAllState()
{
if (toggleAllCheckbox != null && !SelectionAnimationRunning)
{
@@ -399,7 +433,7 @@ namespace osu.Game.Overlays.Mods
foreach (var modPanel in panelFlow)
modPanel.ApplyFilter(Filter);
- updateToggleState();
+ updateToggleAllState();
}
#endregion
diff --git a/osu.Game/Overlays/Mods/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs
new file mode 100644
index 0000000000..62080ec1b5
--- /dev/null
+++ b/osu.Game/Overlays/Mods/ModSelectScreen.cs
@@ -0,0 +1,392 @@
+// 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.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Framework.Layout;
+using osu.Game.Configuration;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets.Mods;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Overlays.Mods
+{
+ public class ModSelectScreen : OsuFocusedOverlayContainer
+ {
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
+
+ [Cached]
+ public Bindable> SelectedMods { get; private set; } = new Bindable>(Array.Empty());
+
+ protected override bool StartHidden => true;
+
+ private readonly BindableBool customisationVisible = new BindableBool();
+
+ private DifficultyMultiplierDisplay multiplierDisplay;
+ private ModSettingsArea modSettingsArea;
+ private FillFlowContainer columnFlow;
+ private GridContainer grid;
+ private Container mainContent;
+
+ private PopupScreenTitle header;
+ private Container footer;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.Both;
+ RelativePositionAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ mainContent = new Container
+ {
+ Origin = Anchor.BottomCentre,
+ Anchor = Anchor.BottomCentre,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ grid = new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize),
+ new Dimension(GridSizeMode.AutoSize),
+ new Dimension(),
+ new Dimension(GridSizeMode.Absolute, 75),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ header = new PopupScreenTitle
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Title = "Mod Select",
+ Description = "Mods provide different ways to enjoy gameplay. Some have an effect on the score you can achieve during ranked play. Others are just for fun.",
+ Close = Hide
+ }
+ },
+ new Drawable[]
+ {
+ new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.X,
+ RelativePositionAxes = Axes.X,
+ X = 0.3f,
+ Height = DifficultyMultiplierDisplay.HEIGHT,
+ Margin = new MarginPadding
+ {
+ Horizontal = 100,
+ Vertical = 10
+ },
+ Child = multiplierDisplay = new DifficultyMultiplierDisplay
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ }
+ }
+ },
+ new Drawable[]
+ {
+ new Container
+ {
+ Depth = float.MaxValue,
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new OsuScrollContainer(Direction.Horizontal)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = false,
+ ClampExtension = 100,
+ ScrollbarOverlapsContent = false,
+ Child = columnFlow = new ModColumnContainer
+ {
+ Direction = FillDirection.Horizontal,
+ RelativeSizeAxes = Axes.Y,
+ AutoSizeAxes = Axes.X,
+ Spacing = new Vector2(10, 0),
+ Margin = new MarginPadding { Right = 70 },
+ Children = new[]
+ {
+ new ModColumn(ModType.DifficultyReduction, false, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }),
+ new ModColumn(ModType.DifficultyIncrease, false, new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }),
+ new ModColumn(ModType.Automation, false, new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }),
+ new ModColumn(ModType.Conversion, false),
+ new ModColumn(ModType.Fun, false)
+ }
+ }
+ }
+ }
+ }
+ },
+ new[] { Empty() }
+ }
+ },
+ footer = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 50,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Colour = colourProvider.Background5
+ },
+ new ShearedToggleButton(200)
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Margin = new MarginPadding { Vertical = 14, Left = 70 },
+ Text = "Mod Customisation",
+ Active = { BindTarget = customisationVisible }
+ }
+ }
+ },
+ new ClickToReturnContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ HandleMouse = { BindTarget = customisationVisible },
+ OnClicked = () => customisationVisible.Value = false
+ }
+ }
+ },
+ modSettingsArea = new ModSettingsArea
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Height = 0
+ }
+ };
+
+ columnFlow.Shear = new Vector2(ModPanel.SHEAR_X, 0);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ ((IBindable>)modSettingsArea.SelectedMods).BindTo(SelectedMods);
+
+ SelectedMods.BindValueChanged(val =>
+ {
+ updateMultiplier();
+ updateCustomisation(val);
+ updateSelectionFromBindable();
+ }, true);
+
+ foreach (var column in columnFlow)
+ {
+ column.SelectedMods.BindValueChanged(_ => updateBindableFromSelection());
+ }
+
+ customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true);
+ }
+
+ private void updateMultiplier()
+ {
+ double multiplier = 1.0;
+
+ foreach (var mod in SelectedMods.Value)
+ multiplier *= mod.ScoreMultiplier;
+
+ multiplierDisplay.Current.Value = multiplier;
+ }
+
+ private void updateCustomisation(ValueChangedEvent> valueChangedEvent)
+ {
+ bool anyCustomisableMod = false;
+ bool anyModWithRequiredCustomisationAdded = false;
+
+ foreach (var mod in SelectedMods.Value)
+ {
+ anyCustomisableMod |= mod.GetSettingsSourceProperties().Any();
+ anyModWithRequiredCustomisationAdded |= !valueChangedEvent.OldValue.Contains(mod) && mod.RequiresConfiguration;
+ }
+
+ if (anyCustomisableMod)
+ {
+ customisationVisible.Disabled = false;
+
+ if (anyModWithRequiredCustomisationAdded && !customisationVisible.Value)
+ customisationVisible.Value = true;
+ }
+ else
+ {
+ if (customisationVisible.Value)
+ customisationVisible.Value = false;
+
+ customisationVisible.Disabled = true;
+ }
+ }
+
+ private void updateCustomisationVisualState()
+ {
+ const double transition_duration = 300;
+
+ grid.FadeColour(customisationVisible.Value ? Colour4.Gray : Colour4.White, transition_duration, Easing.InOutCubic);
+
+ float modAreaHeight = customisationVisible.Value ? ModSettingsArea.HEIGHT : 0;
+
+ modSettingsArea.ResizeHeightTo(modAreaHeight, transition_duration, Easing.InOutCubic);
+ mainContent.TransformTo(nameof(Margin), new MarginPadding { Bottom = modAreaHeight }, transition_duration, Easing.InOutCubic);
+ }
+
+ private bool selectionBindableSyncInProgress;
+
+ private void updateSelectionFromBindable()
+ {
+ if (selectionBindableSyncInProgress)
+ return;
+
+ selectionBindableSyncInProgress = true;
+
+ foreach (var column in columnFlow)
+ column.SelectedMods.Value = SelectedMods.Value.Where(mod => mod.Type == column.ModType).ToArray();
+
+ selectionBindableSyncInProgress = false;
+ }
+
+ private void updateBindableFromSelection()
+ {
+ if (selectionBindableSyncInProgress)
+ return;
+
+ selectionBindableSyncInProgress = true;
+
+ SelectedMods.Value = columnFlow.SelectMany(column => column.SelectedMods.Value).ToArray();
+
+ selectionBindableSyncInProgress = false;
+ }
+
+ protected override void PopIn()
+ {
+ const double fade_in_duration = 400;
+
+ base.PopIn();
+ this.FadeIn(fade_in_duration, Easing.OutQuint);
+
+ header.MoveToY(0, fade_in_duration, Easing.OutQuint);
+ footer.MoveToY(0, fade_in_duration, Easing.OutQuint);
+
+ multiplierDisplay
+ .Delay(fade_in_duration * 0.65f)
+ .FadeIn(fade_in_duration / 2, Easing.OutQuint)
+ .ScaleTo(1, fade_in_duration, Easing.OutElastic);
+
+ for (int i = 0; i < columnFlow.Count; i++)
+ {
+ columnFlow[i].TopLevelContent
+ .Delay(i * 30)
+ .MoveToY(0, fade_in_duration, Easing.OutQuint)
+ .FadeIn(fade_in_duration, Easing.OutQuint);
+ }
+ }
+
+ protected override void PopOut()
+ {
+ const double fade_out_duration = 500;
+
+ base.PopOut();
+ this.FadeOut(fade_out_duration, Easing.OutQuint);
+
+ multiplierDisplay
+ .FadeOut(fade_out_duration / 2, Easing.OutQuint)
+ .ScaleTo(0.75f, fade_out_duration, Easing.OutQuint);
+
+ header.MoveToY(-header.DrawHeight, fade_out_duration, Easing.OutQuint);
+ footer.MoveToY(footer.DrawHeight, fade_out_duration, Easing.OutQuint);
+
+ for (int i = 0; i < columnFlow.Count; i++)
+ {
+ const float distance = 700;
+
+ columnFlow[i].TopLevelContent
+ .MoveToY(i % 2 == 0 ? -distance : distance, fade_out_duration, Easing.OutQuint)
+ .FadeOut(fade_out_duration, Easing.OutQuint);
+ }
+ }
+
+ private class ModColumnContainer : FillFlowContainer
+ {
+ private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize);
+
+ public ModColumnContainer()
+ {
+ AddLayout(drawSizeLayout);
+ }
+
+ public override void Add(ModColumn column)
+ {
+ base.Add(column);
+
+ Debug.Assert(column != null);
+ column.Shear = Vector2.Zero;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!drawSizeLayout.IsValid)
+ {
+ Padding = new MarginPadding
+ {
+ Left = DrawHeight * ModPanel.SHEAR_X,
+ Bottom = 10
+ };
+
+ drawSizeLayout.Validate();
+ }
+ }
+ }
+
+ private class ClickToReturnContainer : Container
+ {
+ public BindableBool HandleMouse { get; } = new BindableBool();
+
+ public Action OnClicked { get; set; }
+
+ protected override bool Handle(UIEvent e)
+ {
+ if (!HandleMouse.Value)
+ return base.Handle(e);
+
+ switch (e)
+ {
+ case ClickEvent _:
+ OnClicked?.Invoke();
+ return true;
+
+ case MouseEvent _:
+ return true;
+ }
+
+ return base.Handle(e);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Mods/ModSettingsArea.cs b/osu.Game/Overlays/Mods/ModSettingsArea.cs
index e0a30f60c2..be72c1e3e3 100644
--- a/osu.Game/Overlays/Mods/ModSettingsArea.cs
+++ b/osu.Game/Overlays/Mods/ModSettingsArea.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Overlays.Mods
{
public Bindable> SelectedMods { get; } = new Bindable>();
+ public const float HEIGHT = 250;
+
private readonly Box background;
private readonly FillFlowContainer modSettingsFlow;
@@ -32,7 +34,7 @@ namespace osu.Game.Overlays.Mods
public ModSettingsArea()
{
RelativeSizeAxes = Axes.X;
- Height = 250;
+ Height = HEIGHT;
Anchor = Anchor.BottomRight;
Origin = Anchor.BottomRight;
@@ -52,6 +54,7 @@ namespace osu.Game.Overlays.Mods
{
RelativeSizeAxes = Axes.Both,
ScrollbarOverlapsContent = false,
+ ClampExtension = 100,
Child = modSettingsFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.X,
@@ -155,9 +158,10 @@ namespace osu.Game.Overlays.Mods
new[] { Empty() },
new Drawable[]
{
- new OsuScrollContainer(Direction.Vertical)
+ new NestedVerticalScrollContainer
{
RelativeSizeAxes = Axes.Both,
+ ClampExtension = 100,
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
diff --git a/osu.Game/Overlays/Mods/NestedVerticalScrollContainer.cs b/osu.Game/Overlays/Mods/NestedVerticalScrollContainer.cs
new file mode 100644
index 0000000000..aba47d5423
--- /dev/null
+++ b/osu.Game/Overlays/Mods/NestedVerticalScrollContainer.cs
@@ -0,0 +1,48 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics.Containers;
+
+namespace osu.Game.Overlays.Mods
+{
+ ///
+ /// A scroll container that handles the case of vertically scrolling content inside a larger horizontally scrolling parent container.
+ ///
+ public class NestedVerticalScrollContainer : OsuScrollContainer
+ {
+ private OsuScrollContainer? parentScrollContainer;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ parentScrollContainer = this.FindClosestParent();
+ }
+
+ protected override bool OnScroll(ScrollEvent e)
+ {
+ if (parentScrollContainer == null)
+ return base.OnScroll(e);
+
+ bool topRightInView = parentScrollContainer.ScreenSpaceDrawQuad.Contains(ScreenSpaceDrawQuad.TopRight);
+ bool bottomLeftInView = parentScrollContainer.ScreenSpaceDrawQuad.Contains(ScreenSpaceDrawQuad.BottomLeft);
+
+ // If not completely on-screen, handle scroll but also allow parent to scroll at the same time (to hopefully bring our content into full view).
+ if (!topRightInView || !bottomLeftInView)
+ return false;
+
+ bool scrollingPastEnd = e.ScrollDelta.Y < 0 && IsScrolledToEnd();
+ bool scrollingPastStart = e.ScrollDelta.Y > 0 && Target <= 0;
+
+ // If at either of our extents, delegate scroll to the horizontal parent container.
+ if (scrollingPastStart || scrollingPastEnd)
+ return false;
+
+ return base.OnScroll(e);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs
index a6b54dd1f2..e569b0e517 100644
--- a/osu.Game/Screens/Menu/IntroScreen.cs
+++ b/osu.Game/Screens/Menu/IntroScreen.cs
@@ -147,7 +147,7 @@ namespace osu.Game.Screens.Menu
bool loadThemedIntro()
{
- var setInfo = beatmaps.QueryBeatmapSet(b => b.Hash == BeatmapHash);
+ var setInfo = beatmaps.QueryBeatmapSet(b => b.Protected && b.Hash == BeatmapHash);
if (setInfo == null)
return false;
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs
index 1201279929..d048676872 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs
@@ -114,18 +114,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating;
- void toggleReady() => Client.ToggleReady().ContinueWith(_ => endOperation());
+ void toggleReady() => Client.ToggleReady().FireAndForget(
+ onSuccess: endOperation,
+ onError: _ => endOperation());
- void startMatch() => Client.StartMatch().ContinueWith(t =>
+ void startMatch() => Client.StartMatch().FireAndForget(onSuccess: () =>
{
- // accessing Exception here silences any potential errors from the antecedent task
- if (t.Exception != null)
- {
- // gameplay was not started due to an exception; unblock button.
- endOperation();
- }
-
// gameplay is starting, the button will be unblocked on load requested.
+ }, onError: _ =>
+ {
+ // gameplay was not started due to an exception; unblock button.
+ endOperation();
});
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs
index d275f309cb..22a0243f8f 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs
@@ -30,13 +30,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private MultiplayerRoom room => multiplayerClient.Room;
private Sample countdownTickSample;
+ private Sample countdownWarnSample;
+ private Sample countdownWarnFinalSample;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
countdownTickSample = audio.Samples.Get(@"Multiplayer/countdown-tick");
- // disabled for now pending further work on sound effect
- // countdownTickFinalSample = audio.Samples.Get(@"Multiplayer/countdown-tick-final");
+ countdownWarnSample = audio.Samples.Get(@"Multiplayer/countdown-warn");
+ countdownWarnFinalSample = audio.Samples.Get(@"Multiplayer/countdown-warn-final");
}
protected override void LoadComplete()
@@ -102,8 +104,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void playTickSound(int secondsRemaining)
{
if (secondsRemaining < 10) countdownTickSample?.Play();
- // disabled for now pending further work on sound effect
- // if (secondsRemaining <= 3) countdownTickFinalSample?.Play();
+
+ if (secondsRemaining <= 3)
+ {
+ if (secondsRemaining > 0)
+ countdownWarnSample?.Play();
+ else
+ countdownWarnFinalSample?.Play();
+ }
}
private void updateButtonText()
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs
index 1653d416d8..d72ce5e960 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs
@@ -62,7 +62,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
{
base.LoadComplete();
- RequestDeletion = item => multiplayerClient.RemovePlaylistItem(item.ID);
+ RequestDeletion = item => multiplayerClient.RemovePlaylistItem(item.ID).FireAndForget();
multiplayerClient.RoomUpdated += onRoomUpdated;
onRoomUpdated();
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs
index 28c9bef3f0..2482d52492 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
// If gameplay wasn't finished, then we have a simple path back to the idle state by aborting gameplay.
if (!playerLoader.GameplayPassed)
{
- client.AbortGameplay();
+ client.AbortGameplay().FireAndForget();
return;
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs
index d49c122bd1..848424bc76 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs
@@ -1,13 +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;
-using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
-using Microsoft.AspNetCore.SignalR;
using osu.Framework.Allocation;
-using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
@@ -76,40 +72,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Task task = itemToEdit != null ? client.EditPlaylistItem(multiplayerItem) : client.AddPlaylistItem(multiplayerItem);
- task.ContinueWith(t =>
+ task.FireAndForget(onSuccess: () => Schedule(() =>
{
- Schedule(() =>
- {
- // If an error or server side trigger occurred this screen may have already exited by external means.
- if (!this.IsCurrentScreen())
- return;
-
- loadingLayer.Hide();
-
- if (t.IsFaulted)
- {
- Exception exception = t.Exception;
-
- if (exception is AggregateException ae)
- exception = ae.InnerException;
-
- Debug.Assert(exception != null);
-
- string message = exception is HubException
- // HubExceptions arrive with additional message context added, but we want to display the human readable message:
- // "An unexpected error occurred invoking 'AddPlaylistItem' on the server.InvalidStateException: Can't enqueue more than 3 items at once."
- // We generally use the message field for a user-parseable error (eventually to be replaced), so drop the first part for now.
- ? exception.Message.Substring(exception.Message.IndexOf(':') + 1).Trim()
- : exception.Message;
-
- Logger.Log(message, level: LogLevel.Important);
- Carousel.AllowSelection = true;
- return;
- }
+ loadingLayer.Hide();
+ // If an error or server side trigger occurred this screen may have already exited by external means.
+ if (this.IsCurrentScreen())
this.Exit();
- });
- });
+ }), onError: _ => Schedule(() =>
+ {
+ loadingLayer.Hide();
+ Carousel.AllowSelection = true;
+ }));
}
else
{
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index e53153e017..7ef0bb5923 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -281,7 +281,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client.Room == null)
return;
- client.ChangeUserMods(mods.NewValue);
+ client.ChangeUserMods(mods.NewValue).FireAndForget();
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
modSettingChangeTracker.SettingChanged += onModSettingsChanged;
@@ -296,7 +296,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client.Room == null)
return;
- client.ChangeUserMods(UserMods.Value);
+ client.ChangeUserMods(UserMods.Value).FireAndForget();
}, 500);
}
@@ -305,7 +305,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client.Room == null)
return;
- client.ChangeBeatmapAvailability(availability.NewValue);
+ client.ChangeBeatmapAvailability(availability.NewValue).FireAndForget();
if (availability.NewValue.State != DownloadState.LocallyAvailable)
{
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
index 7ba0a63856..e091559046 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
@@ -169,7 +169,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
Origin = Anchor.Centre,
Alpha = 0,
Margin = new MarginPadding(4),
- Action = () => Client.KickUser(User.UserID),
+ Action = () => Client.KickUser(User.UserID).FireAndForget(),
},
},
}
@@ -231,7 +231,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
if (!Client.IsHost)
return;
- Client.TransferHost(targetUser);
+ Client.TransferHost(targetUser).FireAndForget();
}),
new OsuMenuItem("Kick", MenuItemType.Destructive, () =>
{
@@ -239,7 +239,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
if (!Client.IsHost)
return;
- Client.KickUser(targetUser);
+ Client.KickUser(targetUser).FireAndForget();
})
};
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs
index 73aca0acdc..aca2c6073a 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs
@@ -83,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
Client.SendMatchRequest(new ChangeTeamRequest
{
TeamID = ((Client.LocalUser?.MatchState as TeamVersusUserState)?.TeamID + 1) % 2 ?? 0,
- });
+ }).FireAndForget();
}
public int? DisplayedTeam { get; private set; }
diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs
index 6a74fdaf75..0c9c909395 100644
--- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs
+++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs
@@ -87,31 +87,33 @@ namespace osu.Game.Screens.Ranking
});
}
- button.Enabled.Value = replayAvailability != ReplayAvailability.NotAvailable;
- updateTooltip();
+ updateState();
}, true);
State.BindValueChanged(state =>
{
button.State.Value = state.NewValue;
- updateTooltip();
+ updateState();
}, true);
}
- private void updateTooltip()
+ private void updateState()
{
switch (replayAvailability)
{
case ReplayAvailability.Local:
button.TooltipText = @"watch replay";
+ button.Enabled.Value = true;
break;
case ReplayAvailability.Online:
button.TooltipText = @"download replay";
+ button.Enabled.Value = true;
break;
default:
button.TooltipText = @"replay unavailable";
+ button.Enabled.Value = false;
break;
}
}
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 92713023f4..f7d5581621 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -443,7 +443,9 @@ namespace osu.Game.Skinning
string lookupName = name.Replace(@"@2x", string.Empty);
float ratio = 2;
- var texture = Textures?.Get(@$"{lookupName}@2x", wrapModeS, wrapModeT);
+ string twoTimesFilename = $"{Path.ChangeExtension(lookupName, null)}@2x{Path.GetExtension(lookupName)}";
+
+ var texture = Textures?.Get(twoTimesFilename, wrapModeS, wrapModeT);
if (texture == null)
{
diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs
index 88cb5f40a1..2ef6f28720 100644
--- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs
+++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs
@@ -5,8 +5,10 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
+using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Utils;
+using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Storyboards.Drawables
@@ -88,6 +90,14 @@ namespace osu.Game.Storyboards.Drawables
LifetimeEnd = animation.EndTime;
}
+ protected override Vector2 GetCurrentDisplaySize()
+ {
+ Texture texture = (CurrentFrame as Sprite)?.Texture
+ ?? ((CurrentFrame as SkinnableSprite)?.Drawable as Sprite)?.Texture;
+
+ return new Vector2(texture?.DisplayWidth ?? 0, texture?.DisplayHeight ?? 0);
+ }
+
[BackgroundDependencyLoader]
private void load(TextureStore textureStore, Storyboard storyboard)
{
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 1bebf78d97..7fd2647a8a 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -29,15 +29,14 @@
-
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index efd5bac38e..a5efc40d51 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,8 +61,8 @@
-
-
+
+
@@ -84,7 +84,7 @@
-
+