diff --git a/Directory.Build.props b/Directory.Build.props
index 734374c840..b08283f071 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -35,7 +35,7 @@
https://github.com/ppy/osu
Automated release.
ppy Pty Ltd
- Copyright (c) 2022 ppy Pty Ltd
+ Copyright (c) 2024 ppy Pty Ltd
osu game
diff --git a/LICENCE b/LICENCE
index d3e7537cef..3bb8b62d5d 100644
--- a/LICENCE
+++ b/LICENCE
@@ -1,4 +1,4 @@
-Copyright (c) 2022 ppy Pty Ltd .
+Copyright (c) 2024 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/README.md b/README.md
index d5dc0723af..d7e710f392 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ A few resources are available as starting points to getting involved and underst
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
- You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management).
-- Track our current efforts [towards full "ranked play" support](https://github.com/orgs/ppy/projects/13?query=is%3Aopen+sort%3Aupdated-desc).
+- Track our current efforts [towards improving the game](https://github.com/orgs/ppy/projects/7/views/6).
## Running osu!
diff --git a/Templates/osu.Game.Templates.csproj b/Templates/osu.Game.Templates.csproj
index b8c3ad373a..186a6093f5 100644
--- a/Templates/osu.Game.Templates.csproj
+++ b/Templates/osu.Game.Templates.csproj
@@ -1,4 +1,4 @@
-
+
Template
ppy.osu.Game.Templates
@@ -8,7 +8,7 @@
https://github.com/ppy/osu/blob/master/Templates
https://github.com/ppy/osu
Automated release.
- Copyright (c) 2022 ppy Pty Ltd
+ Copyright (c) 2024 ppy Pty Ltd
Templates to use when creating a ruleset for consumption in osu!.
dotnet-new;templates;osu
netstandard2.1
diff --git a/assets/lazer-nuget.png b/assets/lazer-nuget.png
index c2a587fdc2..fed2f45149 100644
Binary files a/assets/lazer-nuget.png and b/assets/lazer-nuget.png differ
diff --git a/assets/lazer.png b/assets/lazer.png
index 1e40e844cc..2ee44225bf 100644
Binary files a/assets/lazer.png and b/assets/lazer.png differ
diff --git a/osu.Android.props b/osu.Android.props
index a7376aa5a7..969fd52340 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -10,7 +10,7 @@
true
-
+
diff --git a/osu.Android/Resources/drawable/ic_launcher_background.xml b/osu.Android/Resources/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000000..1af30228ec
--- /dev/null
+++ b/osu.Android/Resources/drawable/ic_launcher_background.xml
@@ -0,0 +1,618 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/osu.Android/Resources/drawable/lazer.png b/osu.Android/Resources/drawable/lazer.png
deleted file mode 100644
index fc7aa8a092..0000000000
Binary files a/osu.Android/Resources/drawable/lazer.png and /dev/null differ
diff --git a/osu.Android/Resources/drawable/monochrome.xml b/osu.Android/Resources/drawable/monochrome.xml
new file mode 100644
index 0000000000..e12af03bfb
--- /dev/null
+++ b/osu.Android/Resources/drawable/monochrome.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
diff --git a/osu.Android/Resources/mipmap-anydpi-v26/ic_launcher.xml b/osu.Android/Resources/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000..7133c9c861
--- /dev/null
+++ b/osu.Android/Resources/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/osu.Android/Resources/mipmap-hdpi/ic_launcher.png b/osu.Android/Resources/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..7870430484
Binary files /dev/null and b/osu.Android/Resources/mipmap-hdpi/ic_launcher.png differ
diff --git a/osu.Android/Resources/mipmap-hdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..b2ec3e49da
Binary files /dev/null and b/osu.Android/Resources/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/osu.Android/Resources/mipmap-mdpi/ic_launcher.png b/osu.Android/Resources/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..2a01d8f781
Binary files /dev/null and b/osu.Android/Resources/mipmap-mdpi/ic_launcher.png differ
diff --git a/osu.Android/Resources/mipmap-mdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..e22f256562
Binary files /dev/null and b/osu.Android/Resources/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/osu.Android/Resources/mipmap-xhdpi/ic_launcher.png b/osu.Android/Resources/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..b5e1a9e379
Binary files /dev/null and b/osu.Android/Resources/mipmap-xhdpi/ic_launcher.png differ
diff --git a/osu.Android/Resources/mipmap-xhdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..1cc3fa9072
Binary files /dev/null and b/osu.Android/Resources/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/osu.Android/Resources/mipmap-xxhdpi/ic_launcher.png b/osu.Android/Resources/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..8a37b0449e
Binary files /dev/null and b/osu.Android/Resources/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/osu.Android/Resources/mipmap-xxhdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..1b856a31b2
Binary files /dev/null and b/osu.Android/Resources/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher.png b/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..65751e15c9
Binary files /dev/null and b/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..05c6829a47
Binary files /dev/null and b/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/osu.Desktop/lazer.ico b/osu.Desktop/lazer.ico
old mode 100755
new mode 100644
index a6aa8abb9f..f84866b8e9
Binary files a/osu.Desktop/lazer.ico and b/osu.Desktop/lazer.ico differ
diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec
index db58c325bd..3b7d6cbe79 100644
--- a/osu.Desktop/osu.nuspec
+++ b/osu.Desktop/osu.nuspec
@@ -7,11 +7,12 @@
ppy Pty Ltd
Dean Herbert
https://osu.ppy.sh/
- https://puu.sh/tYyXZ/9a01a5d1b0.ico
+ https://github.com/ppy/osu/blob/master/assets/lazer-nuget.png?raw=true
+ icon.png
false
A free-to-win rhythm game. Rhythm is just a *click* away!
testing
- Copyright (c) 2022 ppy Pty Ltd
+ Copyright (c) 2024 ppy Pty Ltd
en-AU
diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs
index ccd388192e..e472de1dfe 100644
--- a/osu.Game.Rulesets.Osu/OsuInputManager.cs
+++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs
@@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu
base.ReloadMappings(realmKeyBindings);
if (!AllowGameplayInputs)
- KeyBindings = KeyBindings.Where(b => b.GetAction() == OsuAction.Smoke).ToList();
+ KeyBindings = KeyBindings.Where(static b => b.GetAction() == OsuAction.Smoke).ToList();
}
}
}
diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs
index eae12edebd..95fd2669e5 100644
--- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs
+++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs
@@ -75,8 +75,6 @@ namespace osu.Game.Tests.Chat
return false;
};
});
-
- AddUntilStep("wait for notifications client", () => channelManager.NotificationsConnected);
}
[Test]
diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
index fbe5a0e4d7..98cb66a234 100644
--- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
+++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
@@ -169,9 +169,9 @@ namespace osu.Game.Tests.NonVisual.Skinning
public IRenderer Renderer => new DummyRenderer();
public AudioManager AudioManager => null;
- public IResourceStore Files => null;
- public IResourceStore Resources => null;
- public RealmAccess RealmAccess => null;
+ public IResourceStore Files => null!;
+ public IResourceStore Resources => null!;
+ public RealmAccess RealmAccess => null!;
public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => textureStore;
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
index 3ac4d25028..636cd78d9c 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
@@ -47,8 +47,8 @@ namespace osu.Game.Tests.Visual.Gameplay
seekTo(referenceBeatmap.HitObjects[^1].GetEndTime());
AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true);
- AddAssert("score has combo", () => getResultsScreen().Score.Combo > 100);
- AddAssert("score has no misses", () => getResultsScreen().Score.Statistics[HitResult.Miss] == 0);
+ AddAssert("score has combo", () => getResultsScreen().Score!.Combo > 100);
+ AddAssert("score has no misses", () => getResultsScreen().Score!.Statistics[HitResult.Miss] == 0);
AddUntilStep("avatar displayed", () => getAvatar() != null);
AddAssert("avatar not clickable", () => getAvatar().ChildrenOfType().First().Action == null);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
index fafd1330cc..1660f93384 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
@@ -138,8 +138,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
// Player creates new instance of mods after gameplay to ensure any runtime references to drawables etc. are not retained.
- AddAssert("results screen score has copied mods", () => (Player.GetChildScreen() as ResultsScreen)?.Score.Mods.First(), () => Is.Not.SameAs(playerMods.First()));
- AddAssert("results screen score has matching", () => (Player.GetChildScreen() as ResultsScreen)?.Score.Mods.First(), () => Is.EqualTo(playerMods.First()));
+ AddAssert("results screen score has copied mods", () => (Player.GetChildScreen() as ResultsScreen)?.Score?.Mods.First(), () => Is.Not.SameAs(playerMods.First()));
+ AddAssert("results screen score has matching", () => (Player.GetChildScreen() as ResultsScreen)?.Score?.Mods.First(), () => Is.EqualTo(playerMods.First()));
AddUntilStep("score in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null));
AddUntilStep("databased score has correct mods", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID))!.Mods.First(), () => Is.EqualTo(playerMods.First()));
@@ -184,7 +184,11 @@ namespace osu.Game.Tests.Visual.Gameplay
CreateTest();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
- AddStep("log back in", () => API.Login("username", "password"));
+ AddStep("log back in", () =>
+ {
+ API.Login("username", "password");
+ ((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
+ });
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs
index 0bc71924ce..5fc075ed99 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
+using System.Net;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -9,7 +10,9 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
using osu.Game.Overlays;
+using osu.Game.Overlays.Login;
using osu.Game.Users.Drawables;
using osuTK.Input;
@@ -18,6 +21,8 @@ namespace osu.Game.Tests.Visual.Menus
[TestFixture]
public partial class TestSceneLoginOverlay : OsuManualInputManagerTestScene
{
+ private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
+
private LoginOverlay loginOverlay = null!;
[BackgroundDependencyLoader]
@@ -40,9 +45,69 @@ namespace osu.Game.Tests.Visual.Menus
public void TestLoginSuccess()
{
AddStep("logout", () => API.Logout());
+ assertAPIState(APIState.Offline);
AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password");
AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick());
+
+ assertAPIState(APIState.RequiresSecondFactorAuth);
+ AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType().SingleOrDefault(), () => Is.Not.Null);
+
+ AddStep("set up verification handling", () => dummyAPI.HandleRequest = req =>
+ {
+ switch (req)
+ {
+ case VerifySessionRequest verifySessionRequest:
+ if (verifySessionRequest.VerificationKey == "88800088")
+ verifySessionRequest.TriggerSuccess();
+ else
+ verifySessionRequest.TriggerFailure(new WebException());
+ return true;
+ }
+
+ return false;
+ });
+ AddStep("enter code", () => loginOverlay.ChildrenOfType().First().Text = "88800088");
+ assertAPIState(APIState.Online);
+ AddStep("clear handler", () => dummyAPI.HandleRequest = null);
+ }
+
+ private void assertAPIState(APIState expected) =>
+ AddUntilStep($"login state is {expected}", () => API.State.Value, () => Is.EqualTo(expected));
+
+ [Test]
+ public void TestVerificationFailure()
+ {
+ bool verificationHandled = false;
+ AddStep("reset flag", () => verificationHandled = false);
+ AddStep("logout", () => API.Logout());
+ assertAPIState(APIState.Offline);
+
+ AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password");
+ AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick());
+
+ assertAPIState(APIState.RequiresSecondFactorAuth);
+ AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType().SingleOrDefault(), () => Is.Not.Null);
+
+ AddStep("set up verification handling", () => dummyAPI.HandleRequest = req =>
+ {
+ switch (req)
+ {
+ case VerifySessionRequest verifySessionRequest:
+ if (verifySessionRequest.VerificationKey == "88800088")
+ verifySessionRequest.TriggerSuccess();
+ else
+ verifySessionRequest.TriggerFailure(new WebException());
+ verificationHandled = true;
+ return true;
+ }
+
+ return false;
+ });
+ AddStep("enter code", () => loginOverlay.ChildrenOfType().First().Text = "abcdefgh");
+ AddUntilStep("wait for verification handled", () => verificationHandled);
+ assertAPIState(APIState.RequiresSecondFactorAuth);
+ AddStep("clear handler", () => dummyAPI.HandleRequest = null);
}
[Test]
@@ -78,6 +143,12 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password");
AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick());
+ assertAPIState(APIState.RequiresSecondFactorAuth);
+ AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType().SingleOrDefault(), () => Is.Not.Null);
+
+ AddStep("enter code", () => loginOverlay.ChildrenOfType().First().Text = "88800088");
+ assertAPIState(APIState.Online);
+
AddStep("click on flag", () =>
{
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType().First());
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs
index 2bdfc8959d..f0506ed35c 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs
@@ -16,6 +16,8 @@ namespace osu.Game.Tests.Visual.Menus
[TestFixture]
public partial class TestSceneToolbarUserButton : OsuManualInputManagerTestScene
{
+ private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
+
public TestSceneToolbarUserButton()
{
Container mainContainer;
@@ -69,18 +71,20 @@ namespace osu.Game.Tests.Visual.Menus
[Test]
public void TestLoginLogout()
{
- AddStep("Log out", () => ((DummyAPIAccess)API).Logout());
- AddStep("Log in", () => ((DummyAPIAccess)API).Login("wang", "jang"));
+ AddStep("Log out", () => dummyAPI.Logout());
+ AddStep("Log in", () => dummyAPI.Login("wang", "jang"));
+ AddStep("Authenticate via second factor", () => dummyAPI.AuthenticateSecondFactor("abcdefgh"));
}
[Test]
public void TestStates()
{
- AddStep("Log in", () => ((DummyAPIAccess)API).Login("wang", "jang"));
+ AddStep("Log in", () => dummyAPI.Login("wang", "jang"));
+ AddStep("Authenticate via second factor", () => dummyAPI.AuthenticateSecondFactor("abcdefgh"));
foreach (var state in Enum.GetValues())
{
- AddStep($"Change state to {state}", () => ((DummyAPIAccess)API).SetState(state));
+ AddStep($"Change state to {state}", () => dummyAPI.SetState(state));
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index 747805edc8..8c7576ff52 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -698,7 +698,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
var scoreInfo = ((ResultsScreen)multiplayerComponents.CurrentScreen).Score;
- return !scoreInfo.Passed && scoreInfo.Rank == ScoreRank.F;
+ return scoreInfo?.Passed == false && scoreInfo.Rank == ScoreRank.F;
});
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
index cbeff770c9..aaf85dab7c 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
@@ -3,13 +3,18 @@
#nullable disable
+using System;
+using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play;
@@ -19,14 +24,37 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
private MultiplayerPlayer player;
- [SetUpSteps]
- public override void SetUpSteps()
+ [Test]
+ public void TestGameplay()
{
- base.SetUpSteps();
+ setup();
+ AddUntilStep("wait for gameplay start", () => player.LocalUserPlaying.Value);
+ }
+
+ [Test]
+ public void TestFail()
+ {
+ setup(() => new[] { new OsuModAutopilot() });
+
+ AddUntilStep("wait for gameplay start", () => player.LocalUserPlaying.Value);
+ AddStep("set health zero", () => player.ChildrenOfType().Single().Health.Value = 0);
+ AddUntilStep("wait for fail", () => player.ChildrenOfType().Single().HasFailed);
+ AddAssert("fail animation not shown", () => !player.GameplayState.HasFailed);
+
+ // ensure that even after reaching a failed state, score processor keeps accounting for new hit results.
+ // the testing method used here (autopilot + hold key) is sort-of dodgy, but works enough.
+ AddAssert("score is zero", () => player.GameplayState.ScoreProcessor.TotalScore.Value == 0);
+ AddStep("hold key", () => player.ChildrenOfType().First().TriggerPressed(OsuAction.LeftButton));
+ AddUntilStep("score changed", () => player.GameplayState.ScoreProcessor.TotalScore.Value > 0);
+ }
+
+ private void setup(Func> mods = null)
+ {
AddStep("set beatmap", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+ SelectedMods.Value = mods?.Invoke() ?? Array.Empty();
});
AddStep("Start track playing", () =>
@@ -52,11 +80,5 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("gameplay clock is not paused", () => !player.ChildrenOfType().Single().IsPaused.Value);
AddAssert("gameplay clock is running", () => player.ChildrenOfType().Single().IsRunning);
}
-
- [Test]
- public void TestGameplay()
- {
- AddUntilStep("wait for gameplay start", () => player.LocalUserPlaying.Value);
- }
}
}
diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
index 459a8b0df5..6590339311 100644
--- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
@@ -193,7 +193,7 @@ namespace osu.Game.Tests.Visual.Navigation
case ScorePresentType.Results:
AddUntilStep("wait for results", () => lastWaitedScreen != Game.ScreenStack.CurrentScreen && Game.ScreenStack.CurrentScreen is ResultsScreen);
AddStep("store last waited screen", () => lastWaitedScreen = Game.ScreenStack.CurrentScreen);
- AddUntilStep("correct score displayed", () => ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score.Equals(getImport()));
+ AddUntilStep("correct score displayed", () => ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score!.Equals(getImport()));
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.Equals(getImport().Ruleset));
break;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs
index 0f920643f0..79fb063ea9 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs
@@ -10,6 +10,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
+using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.AccountCreation;
@@ -59,7 +60,11 @@ namespace osu.Game.Tests.Visual.Online
AddStep("click button", () => accountCreation.ChildrenOfType().Single().TriggerClick());
AddUntilStep("warning screen is present", () => accountCreation.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
- AddStep("log back in", () => API.Login("dummy", "password"));
+ AddStep("log back in", () =>
+ {
+ API.Login("dummy", "password");
+ ((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
+ });
AddUntilStep("overlay is hidden", () => accountCreation.State.Value == Visibility.Hidden);
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
index 10fdffb8e1..f47322b9e0 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
@@ -67,6 +67,7 @@ namespace osu.Game.Tests.Visual.Online
Schedule(() =>
{
API.Login("test", "test");
+ dummyAPI.AuthenticateSecondFactor("abcdefgh");
Child = commentsContainer = new CommentsContainer();
});
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneFavouriteButton.cs b/osu.Game.Tests/Visual/Online/TestSceneFavouriteButton.cs
index 3954fd5cff..0acf8336e3 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneFavouriteButton.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneFavouriteButton.cs
@@ -6,6 +6,7 @@
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
+using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet.Buttons;
using osuTK;
@@ -34,14 +35,22 @@ namespace osu.Game.Tests.Visual.Online
AddStep("set valid beatmap", () => favourite.BeatmapSet.Value = new APIBeatmapSet { OnlineID = 88 });
AddStep("log out", () => API.Logout());
checkEnabled(false);
- AddStep("log in", () => API.Login("test", "test"));
+ AddStep("log in", () =>
+ {
+ API.Login("test", "test");
+ ((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
+ });
checkEnabled(true);
}
[Test]
public void TestBeatmapChange()
{
- AddStep("log in", () => API.Login("test", "test"));
+ AddStep("log in", () =>
+ {
+ API.Login("test", "test");
+ ((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
+ });
AddStep("set valid beatmap", () => favourite.BeatmapSet.Value = new APIBeatmapSet { OnlineID = 88 });
checkEnabled(true);
AddStep("set invalid beatmap", () => favourite.BeatmapSet.Value = new APIBeatmapSet());
diff --git a/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs b/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs
index be819afa3e..3607b37c7e 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs
@@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.Online
else
{
int userId = int.Parse(getUserRequest.Lookup);
- string rulesetName = getUserRequest.Ruleset.ShortName;
+ string rulesetName = getUserRequest.Ruleset!.ShortName;
var response = new APIUser
{
Id = userId,
@@ -177,7 +177,11 @@ namespace osu.Game.Tests.Visual.Online
AddWaitStep("wait a bit", 5);
AddAssert("update not received", () => update == null);
- AddStep("log in user", () => dummyAPI.Login("user", "password"));
+ AddStep("log in user", () =>
+ {
+ dummyAPI.Login("user", "password");
+ dummyAPI.AuthenticateSecondFactor("abcdefgh");
+ });
}
[Test]
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
index 1375689075..bc8f75d4ce 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
@@ -52,7 +52,11 @@ namespace osu.Game.Tests.Visual.Online
AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
AddToggleStep("toggle visibility", visible => profile.State.Value = visible ? Visibility.Visible : Visibility.Hidden);
AddStep("log out", () => dummyAPI.Logout());
- AddStep("log back in", () => dummyAPI.Login("username", "password"));
+ AddStep("log back in", () =>
+ {
+ dummyAPI.Login("username", "password");
+ dummyAPI.AuthenticateSecondFactor("abcdefgh");
+ });
}
[Test]
@@ -98,7 +102,11 @@ namespace osu.Game.Tests.Visual.Online
});
AddStep("logout", () => dummyAPI.Logout());
AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
- AddStep("login", () => dummyAPI.Login("username", "password"));
+ AddStep("login", () =>
+ {
+ dummyAPI.Login("username", "password");
+ dummyAPI.AuthenticateSecondFactor("abcdefgh");
+ });
AddWaitStep("wait some", 3);
AddStep("complete request", () => pendingRequest.TriggerSuccess(TEST_USER));
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs b/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs
index ce1a9ac6a7..488902c417 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs
@@ -11,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Game.Overlays;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Containers;
+using osu.Game.Online.API;
namespace osu.Game.Tests.Visual.Online
{
@@ -72,7 +73,11 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("Login overlay is visible", () => login.State.Value == Visibility.Visible);
}
- private void logIn() => API.Login("localUser", "password");
+ private void logIn()
+ {
+ API.Login("localUser", "password");
+ ((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
+ }
private Comment getUserComment() => new Comment
{
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
index e805b03542..25ee20b089 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
@@ -420,7 +420,7 @@ namespace osu.Game.Tests.Visual.Playlists
public new LoadingSpinner RightSpinner => base.RightSpinner;
public new ScorePanelList ScorePanelList => base.ScorePanelList;
- public TestResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
+ public TestResultsScreen([CanBeNull] ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
: base(score, roomId, playlistItem, allowRetry)
{
}
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
index 866e20d063..5671cbebd7 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
@@ -418,7 +418,7 @@ namespace osu.Game.Tests.Visual.Ranking
public UnrankedSoloResultsScreen(ScoreInfo score)
: base(score, true)
{
- Score.BeatmapInfo!.OnlineID = 0;
+ Score!.BeatmapInfo!.OnlineID = 0;
Score.BeatmapInfo.Status = BeatmapOnlineStatus.Pending;
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs
index b17024ae8f..e1d40882be 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs
@@ -125,7 +125,11 @@ namespace osu.Game.Tests.Visual.UserInterface
assertLoggedOutState();
// moving from logged out -> logged in
- AddStep("log back in", () => dummyAPI.Login("username", "password"));
+ AddStep("log back in", () =>
+ {
+ dummyAPI.Login("username", "password");
+ dummyAPI.AuthenticateSecondFactor("abcdefgh");
+ });
assertLoggedInState();
}
diff --git a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs
index 706b05f5ad..4143a6d76d 100644
--- a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs
+++ b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs
@@ -1,17 +1,17 @@
// 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.Utils;
-using osuTK;
using System;
-using osu.Framework.Graphics.Shaders;
-using osu.Framework.Graphics.Textures;
-using osu.Framework.Graphics.Primitives;
-using osu.Framework.Allocation;
using System.Collections.Generic;
-using osu.Framework.Graphics.Rendering;
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Rendering;
+using osu.Framework.Graphics.Shaders;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Utils;
+using osuTK;
namespace osu.Game.Graphics.Backgrounds
{
@@ -27,6 +27,8 @@ namespace osu.Game.Graphics.Backgrounds
public float Thickness { get; set; } = 0.02f; // No need for invalidation since it's happening in Update()
+ public float ScaleAdjust { get; set; } = 1;
+
///
/// Whether we should create new triangles as others expire.
///
@@ -106,7 +108,7 @@ namespace osu.Game.Graphics.Backgrounds
parts[i] = newParticle;
- float bottomPos = parts[i].Position.Y + triangle_size * equilateral_triangle_ratio / DrawHeight;
+ float bottomPos = parts[i].Position.Y + triangle_size * ScaleAdjust * equilateral_triangle_ratio / DrawHeight;
if (bottomPos < 0)
parts.RemoveAt(i);
}
@@ -149,7 +151,7 @@ namespace osu.Game.Graphics.Backgrounds
if (randomY)
{
// since triangles are drawn from the top - allow them to be positioned a bit above the screen
- float maxOffset = triangle_size * equilateral_triangle_ratio / DrawHeight;
+ float maxOffset = triangle_size * ScaleAdjust * equilateral_triangle_ratio / DrawHeight;
y = Interpolation.ValueAt(nextRandom(), -maxOffset, 1f, 0f, 1f);
}
@@ -188,7 +190,7 @@ namespace osu.Game.Graphics.Backgrounds
private readonly List parts = new List();
- private readonly Vector2 triangleSize = new Vector2(1f, equilateral_triangle_ratio) * triangle_size;
+ private Vector2 triangleSize;
private Vector2 size;
private float thickness;
@@ -209,6 +211,7 @@ namespace osu.Game.Graphics.Backgrounds
size = Source.DrawSize;
thickness = Source.Thickness;
clampAxes = Source.ClampAxes;
+ triangleSize = new Vector2(1f, equilateral_triangle_ratio) * triangle_size * Source.ScaleAdjust;
Quad triangleQuad = new Quad(
Vector2Extensions.Transform(Vector2.Zero, DrawInfo.Matrix),
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 17bf8bcc37..d3707fe74d 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -21,7 +21,7 @@ using osu.Game.Configuration;
using osu.Game.Localisation;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Online.Notifications;
+using osu.Game.Online.Chat;
using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Users;
@@ -48,6 +48,8 @@ namespace osu.Game.Online.API
public string ProvidedUsername { get; private set; }
+ public string SecondFactorCode { get; private set; }
+
private string password;
public IBindable LocalUser => localUser;
@@ -55,6 +57,8 @@ namespace osu.Game.Online.API
public IBindable Activity => activity;
public IBindable Statistics => statistics;
+ public INotificationsClient NotificationsClient { get; }
+
public Language Language => game.CurrentLanguage.Value;
private Bindable localUser { get; } = new Bindable(createGuestUser());
@@ -82,6 +86,7 @@ namespace osu.Game.Online.API
APIEndpointUrl = endpointConfiguration.APIEndpointUrl;
WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl;
+ NotificationsClient = setUpNotificationsClient();
authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl);
log = Logger.GetLogger(LoggingTarget.Network);
@@ -114,6 +119,30 @@ namespace osu.Game.Online.API
thread.Start();
}
+ private WebSocketNotificationsClientConnector setUpNotificationsClient()
+ {
+ var connector = new WebSocketNotificationsClientConnector(this);
+
+ connector.MessageReceived += msg =>
+ {
+ switch (msg.Event)
+ {
+ case @"verified":
+ if (state.Value == APIState.RequiresSecondFactorAuth)
+ state.Value = APIState.Online;
+ break;
+
+ case @"logout":
+ if (state.Value == APIState.Online)
+ Logout();
+
+ break;
+ }
+ };
+
+ return connector;
+ }
+
private void onTokenChanged(ValueChangedEvent e) => config.SetValue(OsuSetting.Token, config.Get(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty);
internal new void Schedule(Action action) => base.Schedule(action);
@@ -197,6 +226,7 @@ namespace osu.Game.Online.API
///
///
/// This method takes control of and transitions from to either
+ /// - (pending 2fa)
/// - (successful connection)
/// - (failed connection but retrying)
/// - (failed and can't retry, clear credentials and require user interaction)
@@ -204,8 +234,6 @@ namespace osu.Game.Online.API
/// Whether the connection attempt was successful.
private void attemptConnect()
{
- state.Value = APIState.Connecting;
-
if (localUser.IsDefault)
{
// Show a placeholder user if saved credentials are available.
@@ -223,6 +251,7 @@ namespace osu.Game.Online.API
if (!authentication.HasValidAccessToken)
{
+ state.Value = APIState.Connecting;
LastLoginError = null;
try
@@ -240,40 +269,79 @@ namespace osu.Game.Online.API
}
}
- var userReq = new GetUserRequest();
- userReq.Failure += ex =>
+ switch (state.Value)
{
- if (ex is APIException)
+ case APIState.RequiresSecondFactorAuth:
{
- LastLoginError = ex;
- log.Add($@"Login failed for username {ProvidedUsername} on user retrieval ({LastLoginError.Message})!");
- Logout();
+ if (string.IsNullOrEmpty(SecondFactorCode))
+ return;
+
+ state.Value = APIState.Connecting;
+ LastLoginError = null;
+
+ var verificationRequest = new VerifySessionRequest(SecondFactorCode);
+
+ verificationRequest.Success += () => state.Value = APIState.Online;
+ verificationRequest.Failure += ex =>
+ {
+ state.Value = APIState.RequiresSecondFactorAuth;
+ LastLoginError = ex;
+ SecondFactorCode = null;
+ };
+
+ if (!handleRequest(verificationRequest))
+ {
+ state.Value = APIState.Failing;
+ return;
+ }
+
+ if (state.Value != APIState.Online)
+ return;
+
+ break;
}
- else if (ex is WebException webException && webException.Message == @"Unauthorized")
+
+ default:
{
- log.Add(@"Login no longer valid");
- Logout();
+ var userReq = new GetMeRequest();
+
+ userReq.Failure += ex =>
+ {
+ if (ex is APIException)
+ {
+ LastLoginError = ex;
+ log.Add($@"Login failed for username {ProvidedUsername} on user retrieval ({LastLoginError.Message})!");
+ Logout();
+ }
+ else if (ex is WebException webException && webException.Message == @"Unauthorized")
+ {
+ log.Add(@"Login no longer valid");
+ Logout();
+ }
+ else
+ {
+ state.Value = APIState.Failing;
+ }
+ };
+
+ userReq.Success += me =>
+ {
+ me.Status.Value = configStatus.Value ?? UserStatus.Online;
+
+ setLocalUser(me);
+
+ state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth;
+ failureCount = 0;
+ };
+
+ if (!handleRequest(userReq))
+ {
+ state.Value = APIState.Failing;
+ return;
+ }
+
+ break;
}
- else
- {
- state.Value = APIState.Failing;
- }
- };
- userReq.Success += user =>
- {
- user.Status.Value = configStatus.Value ?? UserStatus.Online;
-
- setLocalUser(user);
-
- // we're connected!
- state.Value = APIState.Online;
- failureCount = 0;
- };
-
- if (!handleRequest(userReq))
- {
- state.Value = APIState.Failing;
- return;
}
var friendsReq = new GetFriendsRequest();
@@ -321,11 +389,17 @@ namespace osu.Game.Online.API
this.password = password;
}
+ public void AuthenticateSecondFactor(string code)
+ {
+ Debug.Assert(State.Value == APIState.RequiresSecondFactorAuth);
+
+ SecondFactorCode = code;
+ }
+
public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) =>
new HubClientConnector(clientName, endpoint, this, versionHash, preferMessagePack);
- public NotificationsClientConnector GetNotificationsConnector() =>
- new WebSocketNotificationsClientConnector(this);
+ public IChatClient GetChatClient() => new WebSocketChatClient(this);
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
{
@@ -507,6 +581,7 @@ namespace osu.Game.Online.API
public void Logout()
{
password = null;
+ SecondFactorCode = null;
authentication.Clear();
// Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present
@@ -566,6 +641,11 @@ namespace osu.Game.Online.API
///
Failing,
+ ///
+ /// Waiting on second factor authentication.
+ ///
+ RequiresSecondFactorAuth,
+
///
/// We are in the process of (re-)connecting.
///
diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs
index 4b4f8061e0..4962838bd9 100644
--- a/osu.Game/Online/API/DummyAPIAccess.cs
+++ b/osu.Game/Online/API/DummyAPIAccess.cs
@@ -7,8 +7,10 @@ using System.Threading.Tasks;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Localisation;
+using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Online.Notifications;
+using osu.Game.Online.Chat;
+using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Tests;
using osu.Game.Users;
@@ -30,6 +32,9 @@ namespace osu.Game.Online.API
public Bindable Statistics { get; } = new Bindable();
+ public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient();
+ INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient;
+
public Language Language => Language.en;
public string AccessToken => "token";
@@ -57,6 +62,7 @@ namespace osu.Game.Online.API
private bool shouldFailNextLogin;
private bool stayConnectingNextLogin;
+ private bool requiredSecondFactorAuth = true;
///
/// The current connectivity state of the API.
@@ -117,13 +123,46 @@ namespace osu.Game.Online.API
Id = DUMMY_USER_ID,
};
+ if (requiredSecondFactorAuth)
+ {
+ state.Value = APIState.RequiresSecondFactorAuth;
+ }
+ else
+ {
+ onSuccessfulLogin();
+ requiredSecondFactorAuth = true;
+ }
+ }
+
+ public void AuthenticateSecondFactor(string code)
+ {
+ var request = new VerifySessionRequest(code);
+ request.Failure += e =>
+ {
+ state.Value = APIState.RequiresSecondFactorAuth;
+ LastLoginError = e;
+ };
+
+ state.Value = APIState.Connecting;
+ LastLoginError = null;
+
+ // if no handler installed / handler can't handle verification, just assume that the server would verify for simplicity.
+ if (HandleRequest?.Invoke(request) != true)
+ onSuccessfulLogin();
+
+ // if a handler did handle this, make sure the verification actually passed.
+ if (request.CompletionState == APIRequestCompletionState.Completed)
+ onSuccessfulLogin();
+ }
+
+ private void onSuccessfulLogin()
+ {
+ state.Value = APIState.Online;
Statistics.Value = new UserStatistics
{
GlobalRank = 1,
CountryRank = 1
};
-
- state.Value = APIState.Online;
}
public void Logout()
@@ -144,7 +183,7 @@ namespace osu.Game.Online.API
public IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null;
- public NotificationsClientConnector GetNotificationsConnector() => new PollingNotificationsClientConnector(this);
+ public IChatClient GetChatClient() => new TestChatClientConnector(this);
public RegistrationRequest.RegistrationRequestErrors? CreateAccount(string email, string username, string password)
{
@@ -159,6 +198,11 @@ namespace osu.Game.Online.API
IBindable IAPIProvider.Activity => Activity;
IBindable IAPIProvider.Statistics => Statistics;
+ ///
+ /// Skip 2FA requirement for next login.
+ ///
+ public void SkipSecondFactor() => requiredSecondFactorAuth = false;
+
///
/// During the next simulated login, the process will fail immediately.
///
diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs
index b58d4a363a..66f124f7c3 100644
--- a/osu.Game/Online/API/IAPIProvider.cs
+++ b/osu.Game/Online/API/IAPIProvider.cs
@@ -6,7 +6,8 @@ using System.Threading.Tasks;
using osu.Framework.Bindables;
using osu.Game.Localisation;
using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Online.Notifications;
+using osu.Game.Online.Chat;
+using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Users;
namespace osu.Game.Online.API
@@ -111,6 +112,12 @@ namespace osu.Game.Online.API
/// The user's password.
void Login(string username, string password);
+ ///
+ /// Provide a second-factor authentication code for authentication.
+ ///
+ /// The 2FA code.
+ void AuthenticateSecondFactor(string code);
+
///
/// Log out the current user.
///
@@ -130,9 +137,14 @@ namespace osu.Game.Online.API
IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack = true);
///
- /// Constructs a new .
+ /// Accesses the used to receive asynchronous notifications from web.
///
- NotificationsClientConnector GetNotificationsConnector();
+ INotificationsClient NotificationsClient { get; }
+
+ ///
+ /// Creates a instance to use in order to chat.
+ ///
+ IChatClient GetChatClient();
///
/// Create a new user account. This is a blocking operation.
diff --git a/osu.Game/Online/API/Requests/GetMeRequest.cs b/osu.Game/Online/API/Requests/GetMeRequest.cs
new file mode 100644
index 0000000000..aab7d7b2f1
--- /dev/null
+++ b/osu.Game/Online/API/Requests/GetMeRequest.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Rulesets;
+
+namespace osu.Game.Online.API.Requests
+{
+ public class GetMeRequest : APIRequest
+ {
+ public readonly IRulesetInfo? Ruleset;
+
+ ///
+ /// Gets the currently logged-in user.
+ ///
+ /// The ruleset to get the user's info for.
+ public GetMeRequest(IRulesetInfo? ruleset = null)
+ {
+ Ruleset = ruleset;
+ }
+
+ protected override string Target => $@"me/{Ruleset?.ShortName}";
+ }
+}
diff --git a/osu.Game/Online/API/Requests/GetUserRequest.cs b/osu.Game/Online/API/Requests/GetUserRequest.cs
index 7dcf75950e..90d3268e75 100644
--- a/osu.Game/Online/API/Requests/GetUserRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUserRequest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
@@ -11,24 +9,17 @@ namespace osu.Game.Online.API.Requests
public class GetUserRequest : APIRequest
{
public readonly string Lookup;
- public readonly IRulesetInfo Ruleset;
+ public readonly IRulesetInfo? Ruleset;
private readonly LookupType lookupType;
- ///
- /// Gets the currently logged-in user.
- ///
- public GetUserRequest()
- {
- }
-
///
/// Gets a user from their ID.
///
/// The user to get.
/// The ruleset to get the user's info for.
- public GetUserRequest(long? userId = null, IRulesetInfo ruleset = null)
+ public GetUserRequest(long? userId = null, IRulesetInfo? ruleset = null)
{
- Lookup = userId.ToString();
+ Lookup = userId.ToString()!;
lookupType = LookupType.Id;
Ruleset = ruleset;
}
@@ -38,14 +29,14 @@ namespace osu.Game.Online.API.Requests
///
/// The user to get.
/// The ruleset to get the user's info for.
- public GetUserRequest(string username = null, IRulesetInfo ruleset = null)
+ public GetUserRequest(string username, IRulesetInfo? ruleset = null)
{
Lookup = username;
lookupType = LookupType.Username;
Ruleset = ruleset;
}
- protected override string Target => Lookup != null ? $@"users/{Lookup}/{Ruleset?.ShortName}?key={lookupType.ToString().ToLowerInvariant()}" : $@"me/{Ruleset?.ShortName}";
+ protected override string Target => $@"users/{Lookup}/{Ruleset?.ShortName}?key={lookupType.ToString().ToLowerInvariant()}";
private enum LookupType
{
diff --git a/osu.Game/Online/API/Requests/ReissueVerificationCodeRequest.cs b/osu.Game/Online/API/Requests/ReissueVerificationCodeRequest.cs
new file mode 100644
index 0000000000..f2a3cf0a16
--- /dev/null
+++ b/osu.Game/Online/API/Requests/ReissueVerificationCodeRequest.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Net.Http;
+using osu.Framework.IO.Network;
+
+namespace osu.Game.Online.API.Requests
+{
+ public class ReissueVerificationCodeRequest : APIRequest
+ {
+ protected override WebRequest CreateWebRequest()
+ {
+ var req = base.CreateWebRequest();
+
+ req.Method = HttpMethod.Post;
+
+ return req;
+ }
+
+ protected override string Target => @"session/verify/reissue";
+ }
+}
diff --git a/osu.Game/Online/API/Requests/Responses/APIMe.cs b/osu.Game/Online/API/Requests/Responses/APIMe.cs
new file mode 100644
index 0000000000..3cbddbe5e7
--- /dev/null
+++ b/osu.Game/Online/API/Requests/Responses/APIMe.cs
@@ -0,0 +1,13 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Newtonsoft.Json;
+
+namespace osu.Game.Online.API.Requests.Responses
+{
+ public class APIMe : APIUser
+ {
+ [JsonProperty("session_verified")]
+ public bool SessionVerified { get; set; }
+ }
+}
diff --git a/osu.Game/Online/API/Requests/VerifySessionRequest.cs b/osu.Game/Online/API/Requests/VerifySessionRequest.cs
new file mode 100644
index 0000000000..b39ec5b79a
--- /dev/null
+++ b/osu.Game/Online/API/Requests/VerifySessionRequest.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Net.Http;
+using osu.Framework.IO.Network;
+
+namespace osu.Game.Online.API.Requests
+{
+ public class VerifySessionRequest : APIRequest
+ {
+ public readonly string VerificationKey;
+
+ public VerifySessionRequest(string verificationKey)
+ {
+ VerificationKey = verificationKey;
+ }
+
+ protected override WebRequest CreateWebRequest()
+ {
+ var req = base.CreateWebRequest();
+
+ req.Method = HttpMethod.Post;
+ req.AddParameter(@"verification_key", VerificationKey);
+
+ return req;
+ }
+
+ protected override string Target => @"session/verify";
+ }
+}
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index 23989caae2..74e85c595c 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -16,7 +16,6 @@ using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Online.Notifications;
using osu.Game.Overlays.Chat.Listing;
namespace osu.Game.Online.Chat
@@ -64,13 +63,8 @@ namespace osu.Game.Online.Chat
///
public IBindableList AvailableChannels => availableChannels;
- ///
- /// Whether the client responsible for channel notifications is connected.
- ///
- public bool NotificationsConnected => connector.IsConnected.Value;
-
private readonly IAPIProvider api;
- private readonly NotificationsClientConnector connector;
+ private readonly IChatClient chatClient;
[Resolved]
private UserLookupCache users { get; set; }
@@ -85,7 +79,7 @@ namespace osu.Game.Online.Chat
{
this.api = api;
- connector = api.GetNotificationsConnector();
+ chatClient = api.GetChatClient();
CurrentChannel.ValueChanged += currentChannelChanged;
}
@@ -93,15 +87,11 @@ namespace osu.Game.Online.Chat
[BackgroundDependencyLoader]
private void load()
{
- connector.ChannelJoined += ch => Schedule(() => joinChannel(ch));
-
- connector.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false));
-
- connector.NewMessages += msgs => Schedule(() => addMessages(msgs));
-
- connector.PresenceReceived += () => Schedule(initializeChannels);
-
- connector.Start();
+ chatClient.ChannelJoined += ch => Schedule(() => joinChannel(ch));
+ chatClient.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false));
+ chatClient.NewMessages += msgs => Schedule(() => addMessages(msgs));
+ chatClient.PresenceReceived += () => Schedule(initializeChannels);
+ chatClient.RequestPresence();
apiState.BindTo(api.State);
apiState.BindValueChanged(_ => SendAck(), true);
@@ -655,7 +645,7 @@ namespace osu.Game.Online.Chat
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
- connector?.Dispose();
+ chatClient?.Dispose();
}
}
diff --git a/osu.Game/Online/Chat/IChatClient.cs b/osu.Game/Online/Chat/IChatClient.cs
new file mode 100644
index 0000000000..290ee22710
--- /dev/null
+++ b/osu.Game/Online/Chat/IChatClient.cs
@@ -0,0 +1,39 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+
+namespace osu.Game.Online.Chat
+{
+ ///
+ /// Interface for consuming online chat.
+ ///
+ public interface IChatClient : IDisposable
+ {
+ ///
+ /// Fired when a has been joined.
+ ///
+ event Action? ChannelJoined;
+
+ ///
+ /// Fired when a has been parted.
+ ///
+ event Action? ChannelParted;
+
+ ///
+ /// Fired when new s have arrived from the server.
+ ///
+ event Action>? NewMessages;
+
+ ///
+ /// Requests presence information from the server.
+ ///
+ void RequestPresence();
+
+ ///
+ /// Fired when the initial user presence information has been received.
+ ///
+ event Action? PresenceReceived;
+ }
+}
diff --git a/osu.Game/Online/Chat/WebSocketChatClient.cs b/osu.Game/Online/Chat/WebSocketChatClient.cs
new file mode 100644
index 0000000000..05d3b7b3ce
--- /dev/null
+++ b/osu.Game/Online/Chat/WebSocketChatClient.cs
@@ -0,0 +1,144 @@
+// 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.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using Newtonsoft.Json;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions;
+using osu.Framework.Logging;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.Notifications.WebSocket;
+
+namespace osu.Game.Online.Chat
+{
+ public class WebSocketChatClient : IChatClient
+ {
+ public event Action? ChannelJoined;
+ public event Action? ChannelParted;
+ public event Action>? NewMessages;
+ public event Action? PresenceReceived;
+
+ private readonly IAPIProvider api;
+ private readonly INotificationsClient client;
+ private readonly ConcurrentDictionary channelsMap = new ConcurrentDictionary();
+
+ public WebSocketChatClient(IAPIProvider api)
+ {
+ this.api = api;
+ client = api.NotificationsClient;
+ client.IsConnected.BindValueChanged(start, true);
+ }
+
+ private void start(ValueChangedEvent connected)
+ {
+ if (!connected.NewValue)
+ return;
+
+ client.MessageReceived += onMessageReceived;
+ client.SendAsync(new StartChatRequest()).WaitSafely();
+ RequestPresence();
+ }
+
+ public void RequestPresence()
+ {
+ var fetchReq = new GetUpdatesRequest(0);
+
+ fetchReq.Success += updates =>
+ {
+ if (updates?.Presence != null)
+ {
+ foreach (var channel in updates.Presence)
+ joinChannel(channel);
+
+ handleMessages(updates.Messages);
+ }
+
+ PresenceReceived?.Invoke();
+ };
+
+ api.Queue(fetchReq);
+ }
+
+ private void onMessageReceived(SocketMessage message)
+ {
+ switch (message.Event)
+ {
+ case @"chat.channel.join":
+ Debug.Assert(message.Data != null);
+
+ Channel? joinedChannel = JsonConvert.DeserializeObject(message.Data.ToString());
+ Debug.Assert(joinedChannel != null);
+
+ joinChannel(joinedChannel);
+ break;
+
+ case @"chat.channel.part":
+ Debug.Assert(message.Data != null);
+
+ Channel? partedChannel = JsonConvert.DeserializeObject(message.Data.ToString());
+ Debug.Assert(partedChannel != null);
+
+ partChannel(partedChannel);
+ break;
+
+ case @"chat.message.new":
+ Debug.Assert(message.Data != null);
+
+ NewChatMessageData? messageData = JsonConvert.DeserializeObject(message.Data.ToString());
+ Debug.Assert(messageData != null);
+
+ foreach (var msg in messageData.Messages)
+ postToChannel(msg);
+
+ break;
+ }
+ }
+
+ private void postToChannel(Message message)
+ {
+ if (channelsMap.TryGetValue(message.ChannelId, out Channel? channel))
+ {
+ joinChannel(channel);
+ NewMessages?.Invoke(new List { message });
+ return;
+ }
+
+ var req = new GetChannelRequest(message.ChannelId);
+
+ req.Success += response =>
+ {
+ joinChannel(channelsMap[message.ChannelId] = response.Channel);
+ NewMessages?.Invoke(new List { message });
+ };
+ req.Failure += ex => Logger.Error(ex, "Failed to join channel");
+
+ api.Queue(req);
+ }
+
+ private void joinChannel(Channel ch)
+ {
+ ch.Joined.Value = true;
+ ChannelJoined?.Invoke(ch);
+ }
+
+ private void partChannel(Channel channel) => ChannelParted?.Invoke(channel);
+
+ private void handleMessages(List? messages)
+ {
+ if (messages == null)
+ return;
+
+ NewMessages?.Invoke(messages);
+ }
+
+ public void Dispose()
+ {
+ client.IsConnected.ValueChanged -= start;
+ client.MessageReceived -= onMessageReceived;
+ }
+ }
+}
diff --git a/osu.Game/Online/Notifications/NotificationsClientConnector.cs b/osu.Game/Online/Notifications/NotificationsClientConnector.cs
deleted file mode 100644
index 34ce186cb8..0000000000
--- a/osu.Game/Online/Notifications/NotificationsClientConnector.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using osu.Game.Online.API;
-using osu.Game.Online.Chat;
-
-namespace osu.Game.Online.Notifications
-{
- ///
- /// An abstract connector or s.
- ///
- public abstract class NotificationsClientConnector : PersistentEndpointClientConnector
- {
- public event Action? ChannelJoined;
- public event Action? ChannelParted;
- public event Action>? NewMessages;
- public event Action? PresenceReceived;
-
- protected NotificationsClientConnector(IAPIProvider api)
- : base(api)
- {
- }
-
- protected sealed override async Task BuildConnectionAsync(CancellationToken cancellationToken)
- {
- var client = await BuildNotificationClientAsync(cancellationToken).ConfigureAwait(false);
-
- client.ChannelJoined = c => ChannelJoined?.Invoke(c);
- client.ChannelParted = c => ChannelParted?.Invoke(c);
- client.NewMessages = m => NewMessages?.Invoke(m);
- client.PresenceReceived = () => PresenceReceived?.Invoke();
-
- return client;
- }
-
- protected abstract Task BuildNotificationClientAsync(CancellationToken cancellationToken);
- }
-}
diff --git a/osu.Game/Online/Notifications/WebSocket/DummyNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/DummyNotificationsClient.cs
new file mode 100644
index 0000000000..c1f3d25be7
--- /dev/null
+++ b/osu.Game/Online/Notifications/WebSocket/DummyNotificationsClient.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using osu.Framework.Bindables;
+
+namespace osu.Game.Online.Notifications.WebSocket
+{
+ public class DummyNotificationsClient : INotificationsClient
+ {
+ public IBindable IsConnected => new BindableBool(true);
+
+ public event Action? MessageReceived;
+
+ public Func? HandleMessage;
+
+ public Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = default)
+ {
+ if (HandleMessage?.Invoke(message) != true)
+ throw new InvalidOperationException($@"{nameof(DummyNotificationsClient)} cannot process this message.");
+
+ return Task.CompletedTask;
+ }
+
+ public void Receive(SocketMessage message) => MessageReceived?.Invoke(message);
+ }
+}
diff --git a/osu.Game/Online/Notifications/WebSocket/INotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/INotificationsClient.cs
new file mode 100644
index 0000000000..9a222d0fdd
--- /dev/null
+++ b/osu.Game/Online/Notifications/WebSocket/INotificationsClient.cs
@@ -0,0 +1,31 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using osu.Framework.Bindables;
+
+namespace osu.Game.Online.Notifications.WebSocket
+{
+ ///
+ /// A client for asynchronous notifications sent by osu-web.
+ ///
+ public interface INotificationsClient
+ {
+ ///
+ /// Whether this is currently connected to a server.
+ ///
+ IBindable IsConnected { get; }
+
+ ///
+ /// Invoked when a new arrives for this client.
+ ///
+ event Action? MessageReceived;
+
+ ///
+ /// Sends a to the notification server.
+ ///
+ Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = default);
+ }
+}
diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs
index 73e5dcec6f..854f46880f 100644
--- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs
+++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net;
using System.Net.WebSockets;
@@ -12,23 +11,20 @@ using System.Threading.Tasks;
using Newtonsoft.Json;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Logging;
-using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
-using osu.Game.Online.Chat;
namespace osu.Game.Online.Notifications.WebSocket
{
///
/// A notifications client which receives events via a websocket.
///
- public class WebSocketNotificationsClient : NotificationsClient
+ public class WebSocketNotificationsClient : PersistentEndpointClient
{
+ public event Action? MessageReceived;
+
private readonly ClientWebSocket socket;
private readonly string endpoint;
- private readonly ConcurrentDictionary channelsMap = new ConcurrentDictionary();
- public WebSocketNotificationsClient(ClientWebSocket socket, string endpoint, IAPIProvider api)
- : base(api)
+ public WebSocketNotificationsClient(ClientWebSocket socket, string endpoint)
{
this.socket = socket;
this.endpoint = endpoint;
@@ -37,11 +33,7 @@ namespace osu.Game.Online.Notifications.WebSocket
public override async Task ConnectAsync(CancellationToken cancellationToken)
{
await socket.ConnectAsync(new Uri(endpoint), cancellationToken).ConfigureAwait(false);
- await sendMessage(new StartChatRequest(), CancellationToken.None).ConfigureAwait(false);
-
runReadLoop(cancellationToken);
-
- await base.ConnectAsync(cancellationToken).ConfigureAwait(false);
}
private void runReadLoop(CancellationToken cancellationToken) => Task.Run(async () =>
@@ -73,7 +65,7 @@ namespace osu.Game.Online.Notifications.WebSocket
break;
}
- await onMessageReceivedAsync(message).ConfigureAwait(false);
+ MessageReceived?.Invoke(message);
}
break;
@@ -105,69 +97,12 @@ namespace osu.Game.Online.Notifications.WebSocket
}
}
- private async Task sendMessage(SocketMessage message, CancellationToken cancellationToken)
+ public async Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = default)
{
if (socket.State != WebSocketState.Open)
return;
- await socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
- }
-
- private async Task onMessageReceivedAsync(SocketMessage message)
- {
- switch (message.Event)
- {
- case @"chat.channel.join":
- Debug.Assert(message.Data != null);
-
- Channel? joinedChannel = JsonConvert.DeserializeObject(message.Data.ToString());
- Debug.Assert(joinedChannel != null);
-
- HandleChannelJoined(joinedChannel);
- break;
-
- case @"chat.channel.part":
- Debug.Assert(message.Data != null);
-
- Channel? partedChannel = JsonConvert.DeserializeObject(message.Data.ToString());
- Debug.Assert(partedChannel != null);
-
- HandleChannelParted(partedChannel);
- break;
-
- case @"chat.message.new":
- Debug.Assert(message.Data != null);
-
- NewChatMessageData? messageData = JsonConvert.DeserializeObject(message.Data.ToString());
- Debug.Assert(messageData != null);
-
- foreach (var msg in messageData.Messages)
- HandleChannelJoined(await getChannel(msg.ChannelId).ConfigureAwait(false));
-
- HandleMessages(messageData.Messages);
- break;
- }
- }
-
- private async Task getChannel(long channelId)
- {
- if (channelsMap.TryGetValue(channelId, out Channel? channel))
- return channel;
-
- var tsc = new TaskCompletionSource();
- var req = new GetChannelRequest(channelId);
-
- req.Success += response =>
- {
- channelsMap[channelId] = response.Channel;
- tsc.SetResult(response.Channel);
- };
-
- req.Failure += ex => tsc.SetException(ex);
-
- API.Queue(req);
-
- return await tsc.Task.ConfigureAwait(false);
+ await socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken ?? CancellationToken.None).ConfigureAwait(false);
}
public override async ValueTask DisposeAsync()
diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs
index f50369a06c..73fe29d441 100644
--- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs
+++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Net;
using System.Net.WebSockets;
using System.Threading;
@@ -13,17 +14,20 @@ namespace osu.Game.Online.Notifications.WebSocket
///
/// A connector for s that receive events via a websocket.
///
- public class WebSocketNotificationsClientConnector : NotificationsClientConnector
+ public class WebSocketNotificationsClientConnector : PersistentEndpointClientConnector, INotificationsClient
{
+ public event Action? MessageReceived;
+
private readonly IAPIProvider api;
public WebSocketNotificationsClientConnector(IAPIProvider api)
: base(api)
{
this.api = api;
+ Start();
}
- protected override async Task BuildNotificationClientAsync(CancellationToken cancellationToken)
+ protected override async Task BuildConnectionAsync(CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource();
@@ -40,7 +44,17 @@ namespace osu.Game.Online.Notifications.WebSocket
if (socket.Options.Proxy != null)
socket.Options.Proxy.Credentials = CredentialCache.DefaultCredentials;
- return new WebSocketNotificationsClient(socket, endpoint, api);
+ var client = new WebSocketNotificationsClient(socket, endpoint);
+ client.MessageReceived += msg => MessageReceived?.Invoke(msg);
+ return client;
+ }
+
+ public Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = default)
+ {
+ if (CurrentConnection is not WebSocketNotificationsClient webSocketClient)
+ return Task.CompletedTask;
+
+ return webSocketClient.SendAsync(message, cancellationToken);
}
}
}
diff --git a/osu.Game/Online/OnlineStatusNotifier.cs b/osu.Game/Online/OnlineStatusNotifier.cs
index c36e4ab894..dda430ce6f 100644
--- a/osu.Game/Online/OnlineStatusNotifier.cs
+++ b/osu.Game/Online/OnlineStatusNotifier.cs
@@ -11,6 +11,7 @@ using osu.Framework.Screens;
using osu.Game.Online.API;
using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Online.Spectator;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
@@ -25,6 +26,8 @@ namespace osu.Game.Online
{
private readonly Func getCurrentScreen;
+ private INotificationsClient notificationsClient = null!;
+
[Resolved]
private MultiplayerClient multiplayerClient { get; set; } = null!;
@@ -55,9 +58,11 @@ namespace osu.Game.Online
private void load(IAPIProvider api)
{
apiState = api.State.GetBoundCopy();
+ notificationsClient = api.NotificationsClient;
multiplayerState = multiplayerClient.IsConnected.GetBoundCopy();
spectatorState = spectatorClient.IsConnected.GetBoundCopy();
+ notificationsClient.MessageReceived += notifyAboutForcedDisconnection;
multiplayerClient.Disconnecting += notifyAboutForcedDisconnection;
spectatorClient.Disconnecting += notifyAboutForcedDisconnection;
metadataClient.Disconnecting += notifyAboutForcedDisconnection;
@@ -127,10 +132,27 @@ namespace osu.Game.Online
});
}
+ private void notifyAboutForcedDisconnection(SocketMessage obj)
+ {
+ if (obj.Event != @"logout") return;
+
+ if (userNotified) return;
+
+ userNotified = true;
+ notificationOverlay?.Post(new SimpleErrorNotification
+ {
+ Icon = FontAwesome.Solid.ExclamationCircle,
+ Text = "You have been logged out due to a change to your account. Please log in again."
+ });
+ }
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
+ if (notificationsClient.IsNotNull())
+ notificationsClient.MessageReceived += notifyAboutForcedDisconnection;
+
if (spectatorClient.IsNotNull())
spectatorClient.Disconnecting -= notifyAboutForcedDisconnection;
diff --git a/osu.Game/Online/OnlineViewContainer.cs b/osu.Game/Online/OnlineViewContainer.cs
index 46f64fbb61..824da152b2 100644
--- a/osu.Game/Online/OnlineViewContainer.cs
+++ b/osu.Game/Online/OnlineViewContainer.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -79,10 +80,14 @@ namespace osu.Game.Online
case APIState.Failing:
case APIState.Connecting:
+ case APIState.RequiresSecondFactorAuth:
PopContentOut(Content);
LoadingSpinner.Show();
placeholder.FadeOut(transform_duration / 2, Easing.OutQuint);
break;
+
+ default:
+ throw new ArgumentOutOfRangeException();
}
});
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 4e465f59df..2208f7d7ca 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -340,10 +340,6 @@ namespace osu.Game
dependencies.Cache(beatmapCache = new BeatmapLookupCache());
base.Content.Add(beatmapCache);
- var scorePerformanceManager = new ScorePerformanceCache();
- dependencies.Cache(scorePerformanceManager);
- base.Content.Add(scorePerformanceManager);
-
dependencies.CacheAs(rulesetConfigCache = new RulesetConfigCache(realm, RulesetStore));
var powerStatus = CreateBatteryInfo();
diff --git a/osu.Game/Overlays/AccountCreationOverlay.cs b/osu.Game/Overlays/AccountCreationOverlay.cs
index ef2e055eae..576ee92b48 100644
--- a/osu.Game/Overlays/AccountCreationOverlay.cs
+++ b/osu.Game/Overlays/AccountCreationOverlay.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@@ -118,12 +119,16 @@ namespace osu.Game.Overlays
break;
case APIState.Connecting:
+ case APIState.RequiresSecondFactorAuth:
break;
case APIState.Online:
scheduledHide?.Cancel();
scheduledHide = Schedule(Hide);
break;
+
+ default:
+ throw new ArgumentOutOfRangeException();
}
}
}
diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs
index 0eef55162f..80dfca93d2 100644
--- a/osu.Game/Overlays/Login/LoginForm.cs
+++ b/osu.Game/Overlays/Login/LoginForm.cs
@@ -32,13 +32,7 @@ namespace osu.Game.Overlays.Login
public Action? RequestHide;
- private void performLogin()
- {
- if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text))
- api.Login(username.Text, password.Text);
- else
- shakeSignIn.Shake();
- }
+ public override bool AcceptsFocus => true;
[BackgroundDependencyLoader(permitNulls: true)]
private void load(OsuConfigManager config, AccountCreationOverlay accountCreation)
@@ -144,7 +138,13 @@ namespace osu.Game.Overlays.Login
}
}
- public override bool AcceptsFocus => true;
+ private void performLogin()
+ {
+ if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text))
+ api.Login(username.Text, password.Text);
+ else
+ shakeSignIn.Shake();
+ }
protected override bool OnClick(ClickEvent e) => true;
diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs
index ea65656eb1..25bf612bc3 100644
--- a/osu.Game/Overlays/Login/LoginPanel.cs
+++ b/osu.Game/Overlays/Login/LoginPanel.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Login
{
private bool bounding = true;
- private LoginForm? form;
+ private Drawable? form;
[Resolved]
private OsuColour colours { get; set; } = null!;
@@ -81,6 +81,10 @@ namespace osu.Game.Overlays.Login
};
break;
+ case APIState.RequiresSecondFactorAuth:
+ Child = form = new SecondFactorAuthForm();
+ break;
+
case APIState.Failing:
case APIState.Connecting:
LinkFlowContainer linkFlow;
diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs
new file mode 100644
index 0000000000..dcd3119f33
--- /dev/null
+++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs
@@ -0,0 +1,147 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Threading.Tasks;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Events;
+using osu.Framework.Logging;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Overlays.Settings;
+using osu.Game.Resources.Localisation.Web;
+using osuTK;
+
+namespace osu.Game.Overlays.Login
+{
+ public partial class SecondFactorAuthForm : Container
+ {
+ private OsuTextBox codeTextBox = null!;
+ private LinkFlowContainer explainText = null!;
+ private ErrorTextFlowContainer errorText = null!;
+
+ private LoadingLayer loading = null!;
+
+ [Resolved]
+ private IAPIProvider api { get; set; } = null!;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ Children = new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, SettingsSection.ITEM_SPACING),
+ Children = new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS },
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0f, SettingsSection.ITEM_SPACING),
+ Children = new Drawable[]
+ {
+ new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular))
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Text = "An email has been sent to you with a verification code. Enter the code.",
+ },
+ codeTextBox = new OsuTextBox
+ {
+ PlaceholderText = "Enter code",
+ RelativeSizeAxes = Axes.X,
+ TabbableContentContainer = this,
+ },
+ explainText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular))
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ },
+ errorText = new ErrorTextFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Alpha = 0,
+ },
+ },
+ },
+ new LinkFlowContainer
+ {
+ Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS },
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ },
+ }
+ },
+ loading = new LoadingLayer(true)
+ {
+ Padding = new MarginPadding { Vertical = -SettingsSection.ITEM_SPACING },
+ }
+ };
+
+ explainText.AddParagraph(UserVerificationStrings.BoxInfoCheckSpam);
+ // We can't support localisable strings with nested links yet. Not sure if we even can (probably need to allow markdown link formatting or something).
+ explainText.AddParagraph("If you can't access your email or have forgotten what you used, please follow the ");
+ explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.WebsiteRootUrl}/home/password-reset");
+ explainText.AddText(". You can also ");
+ explainText.AddLink(UserVerificationStrings.BoxInfoReissueLink, () =>
+ {
+ loading.Show();
+
+ var reissueRequest = new ReissueVerificationCodeRequest();
+ reissueRequest.Failure += ex =>
+ {
+ Logger.Error(ex, @"Failed to retrieve new verification code.");
+ loading.Hide();
+ };
+ reissueRequest.Success += () =>
+ {
+ loading.Hide();
+ };
+
+ Task.Run(() => api.Perform(reissueRequest));
+ });
+ explainText.AddText(" or ");
+ explainText.AddLink(UserVerificationStrings.BoxInfoLogoutLink, () => { api.Logout(); });
+ explainText.AddText(".");
+
+ codeTextBox.Current.BindValueChanged(code =>
+ {
+ if (code.NewValue.Length == 8)
+ {
+ api.AuthenticateSecondFactor(code.NewValue);
+ codeTextBox.Current.Disabled = true;
+ }
+ });
+
+ if (api.LastLoginError?.Message is string error)
+ {
+ errorText.Alpha = 1;
+ errorText.AddErrors(new[] { error });
+ }
+ }
+
+ public override bool AcceptsFocus => true;
+
+ protected override bool OnClick(ClickEvent e) => true;
+
+ protected override void OnFocus(FocusEvent e)
+ {
+ Schedule(() => { GetContainingInputManager().ChangeFocus(codeTextBox); });
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs
index 230974cd59..28521e3331 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs
@@ -99,6 +99,7 @@ namespace osu.Game.Overlays.Toolbar
switch (state.NewValue)
{
+ case APIState.RequiresSecondFactorAuth:
case APIState.Connecting:
TooltipText = ToolbarStrings.Connecting;
spinner.Show();
diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs
index ad9257d4f3..4563c264f7 100644
--- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs
@@ -21,21 +21,29 @@ namespace osu.Game.Rulesets.Difficulty
{
private readonly IBeatmap playableBeatmap;
private readonly BeatmapDifficultyCache difficultyCache;
- private readonly ScorePerformanceCache performanceCache;
- public PerformanceBreakdownCalculator(IBeatmap playableBeatmap, BeatmapDifficultyCache difficultyCache, ScorePerformanceCache performanceCache)
+ public PerformanceBreakdownCalculator(IBeatmap playableBeatmap, BeatmapDifficultyCache difficultyCache)
{
this.playableBeatmap = playableBeatmap;
this.difficultyCache = difficultyCache;
- this.performanceCache = performanceCache;
}
[ItemCanBeNull]
public async Task CalculateAsync(ScoreInfo score, CancellationToken cancellationToken = default)
{
+ var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
+
+ var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator();
+
+ // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value.
+ if (attributes?.Attributes == null || performanceCalculator == null)
+ return null;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
PerformanceAttributes[] performanceArray = await Task.WhenAll(
// compute actual performance
- performanceCache.CalculatePerformanceAsync(score, cancellationToken),
+ performanceCalculator.CalculateAsync(score, attributes.Value.Attributes, cancellationToken),
// compute performance for perfect play
getPerfectPerformance(score, cancellationToken)
).ConfigureAwait(false);
@@ -88,8 +96,12 @@ namespace osu.Game.Rulesets.Difficulty
cancellationToken
).ConfigureAwait(false);
- // ScorePerformanceCache is not used to avoid caching multiple copies of essentially identical perfect performance attributes
- return difficulty == null ? null : ruleset.CreatePerformanceCalculator()?.Calculate(perfectPlay, difficulty.Value.Attributes.AsNonNull());
+ var performanceCalculator = ruleset.CreatePerformanceCalculator();
+
+ if (performanceCalculator == null || difficulty == null)
+ return null;
+
+ return await performanceCalculator.CalculateAsync(perfectPlay, difficulty.Value.Attributes.AsNonNull(), cancellationToken).ConfigureAwait(false);
}, cancellationToken);
}
diff --git a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs
index f5e826f8c7..966da0ff12 100644
--- a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Threading;
+using System.Threading.Tasks;
using osu.Game.Beatmaps;
using osu.Game.Scoring;
@@ -15,6 +17,9 @@ namespace osu.Game.Rulesets.Difficulty
Ruleset = ruleset;
}
+ public Task CalculateAsync(ScoreInfo score, DifficultyAttributes attributes, CancellationToken cancellationToken)
+ => Task.Run(() => CreatePerformanceAttributes(score, attributes), cancellationToken);
+
public PerformanceAttributes Calculate(ScoreInfo score, DifficultyAttributes attributes)
=> CreatePerformanceAttributes(score, attributes);
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index bce28361cb..5662fb2293 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -768,6 +768,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (CurrentSkin != null)
CurrentSkin.SourceChanged -= skinSourceChanged;
+
+ // Safeties against shooting in foot in cases where these are bound by external entities (like playfield) that don't clean up.
+ OnNestedDrawableCreated = null;
+ OnNewResult = null;
+ OnRevertResult = null;
+ DefaultsApplied = null;
+ HitObjectApplied = null;
}
public Bindable AnimationStartTime { get; } = new BindableDouble();
diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs
index ec2a4a31f6..ef8bd08bf4 100644
--- a/osu.Game/Rulesets/Objects/HitObject.cs
+++ b/osu.Game/Rulesets/Objects/HitObject.cs
@@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.Objects
///
/// Invoked after has completed on this .
///
+ // TODO: This has no implicit unbind flow. Currently, if a Playfield manages HitObjects it will leave a bound event on this and cause the
+ // playfield to remain in memory.
public event Action DefaultsApplied;
public readonly Bindable StartTimeBindable = new BindableDouble();
diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index a092829317..9d12daad04 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -181,6 +181,8 @@ namespace osu.Game.Rulesets.Scoring
private readonly List hitEvents = new List();
private HitObject? lastHitObject;
+ public bool ApplyNewJudgementsWhenFailed { get; set; }
+
public ScoreProcessor(Ruleset ruleset)
{
Ruleset = ruleset;
@@ -211,7 +213,7 @@ namespace osu.Game.Rulesets.Scoring
result.ComboAtJudgement = Combo.Value;
result.HighestComboAtJudgement = HighestCombo.Value;
- if (result.FailedAtJudgement)
+ if (result.FailedAtJudgement && !ApplyNewJudgementsWhenFailed)
return;
ScoreResultCounts[result.Type] = ScoreResultCounts.GetValueOrDefault(result.Type) + 1;
@@ -267,7 +269,7 @@ namespace osu.Game.Rulesets.Scoring
Combo.Value = result.ComboAtJudgement;
HighestCombo.Value = result.HighestComboAtJudgement;
- if (result.FailedAtJudgement)
+ if (result.FailedAtJudgement && !ApplyNewJudgementsWhenFailed)
return;
ScoreResultCounts[result.Type] = ScoreResultCounts.GetValueOrDefault(result.Type) - 1;
diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs
index 041c7a13ae..a08c3bab08 100644
--- a/osu.Game/Rulesets/UI/RulesetInputManager.cs
+++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs
@@ -218,7 +218,7 @@ namespace osu.Game.Rulesets.UI
{
base.ReloadMappings(realmKeyBindings);
- KeyBindings = KeyBindings.Where(b => RealmKeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList();
+ KeyBindings = KeyBindings.Where(static b => RealmKeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList();
RealmKeyBindingStore.ClearDuplicateBindings(KeyBindings);
}
}
diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs
deleted file mode 100644
index 1f2b1aeb95..0000000000
--- a/osu.Game/Scoring/ScorePerformanceCache.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using osu.Framework.Allocation;
-using osu.Game.Beatmaps;
-using osu.Game.Database;
-using osu.Game.Rulesets.Difficulty;
-
-namespace osu.Game.Scoring
-{
- ///
- /// A component which performs and acts as a central cache for performance calculations of locally databased scores.
- /// Currently not persisted between game sessions.
- ///
- public partial class ScorePerformanceCache : MemoryCachingComponent
- {
- [Resolved]
- private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
-
- protected override bool CacheNullValues => false;
-
- ///
- /// Calculates performance for the given .
- ///
- /// The score to do the calculation on.
- /// An optional to cancel the operation.
- public Task CalculatePerformanceAsync(ScoreInfo score, CancellationToken token = default) =>
- GetAsync(new PerformanceCacheLookup(score), token);
-
- protected override async Task ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default)
- {
- var score = lookup.ScoreInfo;
-
- var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, token).ConfigureAwait(false);
-
- // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value.
- if (attributes?.Attributes == null)
- return null;
-
- token.ThrowIfCancellationRequested();
-
- return score.Ruleset.CreateInstance().CreatePerformanceCalculator()?.Calculate(score, attributes.Value.Attributes);
- }
-
- public readonly struct PerformanceCacheLookup
- {
- public readonly ScoreInfo ScoreInfo;
-
- public PerformanceCacheLookup(ScoreInfo info)
- {
- ScoreInfo = info;
- }
-
- public override int GetHashCode()
- {
- var hash = new HashCode();
-
- hash.Add(ScoreInfo.Hash);
- hash.Add(ScoreInfo.ID);
-
- return hash.ToHashCode();
- }
- }
- }
-}
diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs
index 75ef8be02e..25101730e7 100644
--- a/osu.Game/Screens/Menu/OsuLogo.cs
+++ b/osu.Game/Screens/Menu/OsuLogo.cs
@@ -10,6 +10,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
@@ -31,15 +32,13 @@ namespace osu.Game.Screens.Menu
///
public partial class OsuLogo : BeatSyncedContainer
{
- public readonly Color4 OsuPink = Color4Extensions.FromHex(@"e967a1");
-
private const double transition_length = 300;
///
/// The osu! logo sprite has a shadow included in its texture.
/// This adjustment vector is used to match the precise edge of the border of the logo.
///
- public static readonly Vector2 SCALE_ADJUST = new Vector2(0.96f);
+ public static readonly Vector2 SCALE_ADJUST = new Vector2(0.94f);
private readonly Sprite logo;
private readonly CircularContainer logoContainer;
@@ -58,7 +57,7 @@ namespace osu.Game.Screens.Menu
private Sample sampleDownbeat;
private readonly Container colourAndTriangles;
- private readonly Triangles triangles;
+ private readonly TrianglesV2 triangles;
///
/// Return value decides whether the logo should play its own sample for the click action.
@@ -184,13 +183,16 @@ namespace osu.Game.Screens.Menu
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = OsuPink,
+ Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex(@"ff66ab"), Color4Extensions.FromHex(@"cc5289")),
},
- triangles = new Triangles
+ triangles = new TrianglesV2
{
- TriangleScale = 4,
- ColourLight = Color4Extensions.FromHex(@"ff7db7"),
- ColourDark = Color4Extensions.FromHex(@"de5b95"),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Thickness = 0.009f,
+ ScaleAdjust = 3,
+ SpawnRatio = 1.4f,
+ Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex(@"ff66ab"), Color4Extensions.FromHex(@"b6346f")),
RelativeSizeAxes = Axes.Both,
},
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
index d9043df1d5..c5c536eae6 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
@@ -67,6 +67,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (!LoadedBeatmapSuccessfully)
return;
+ ScoreProcessor.ApplyNewJudgementsWhenFailed = true;
+
LoadComponentAsync(new GameplayChatDisplay(Room)
{
Expanded = { BindTarget = LeaderboardExpandedState },
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs
index aa72394ac9..6a1924dea2 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
[Resolved]
private RulesetStore rulesets { get; set; }
- public PlaylistsResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true)
+ public PlaylistsResultsScreen([CanBeNull] ScoreInfo score, long roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true)
: base(score, allowRetry, allowWatchingReplay)
{
this.roomId = roomId;
diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs
index cb38854bca..7db3f9fd3c 100644
--- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs
+++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs
@@ -114,12 +114,7 @@ namespace osu.Game.Screens.Play.HUD
protected override void UpdateProgress(double progress, bool isIntro)
{
- bar.TrackTime = GameplayClock.CurrentTime;
-
- if (isIntro)
- bar.CurrentTime = 0;
- else
- bar.CurrentTime = FrameStableClock.CurrentTime;
+ bar.Progress = isIntro ? 0 : progress;
}
}
}
diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs
index beaee0e9ee..7a7870a775 100644
--- a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs
+++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs
@@ -3,96 +3,59 @@
using System;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
-using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
- public partial class ArgonSongProgressBar : SliderBar
+ public partial class ArgonSongProgressBar : SongProgressBar
{
- public Action? OnSeek { get; set; }
-
// Parent will handle restricting the area of valid input.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
private readonly float barHeight;
private readonly RoundedBar playfieldBar;
- private readonly RoundedBar catchupBar;
+ private readonly RoundedBar audioBar;
private readonly Box background;
private readonly ColourInfo mainColour;
private ColourInfo catchUpColour;
- public double StartTime
- {
- private get => CurrentNumber.MinValue;
- set => CurrentNumber.MinValue = value;
- }
+ public double Progress { get; set; }
- public double EndTime
- {
- private get => CurrentNumber.MaxValue;
- set => CurrentNumber.MaxValue = value;
- }
-
- public double CurrentTime
- {
- private get => CurrentNumber.Value;
- set => CurrentNumber.Value = value;
- }
-
- public double TrackTime
- {
- private get => currentTrackTime.Value;
- set => currentTrackTime.Value = value;
- }
-
- private double length => EndTime - StartTime;
-
- private readonly BindableNumber currentTrackTime;
-
- public bool Interactive { get; set; }
+ private double trackTime => (EndTime - StartTime) * Progress;
public ArgonSongProgressBar(float barHeight)
{
- currentTrackTime = new BindableDouble();
- setupAlternateValue();
-
- StartTime = 0;
- EndTime = 1;
-
RelativeSizeAxes = Axes.X;
Height = this.barHeight = barHeight;
CornerRadius = 5;
Masking = true;
- Children = new Drawable[]
+ InternalChildren = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Colour = OsuColour.Gray(0.2f),
+ Depth = float.MaxValue,
},
- catchupBar = new RoundedBar
+ audioBar = new RoundedBar
{
Name = "Audio bar",
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
CornerRadius = 5,
- AlwaysPresent = true,
RelativeSizeAxes = Axes.Both
},
playfieldBar = new RoundedBar
@@ -107,24 +70,6 @@ namespace osu.Game.Screens.Play.HUD
};
}
- private void setupAlternateValue()
- {
- CurrentNumber.MaxValueChanged += v => currentTrackTime.MaxValue = v;
- CurrentNumber.MinValueChanged += v => currentTrackTime.MinValue = v;
- CurrentNumber.PrecisionChanged += v => currentTrackTime.Precision = v;
- }
-
- private float normalizedReference
- {
- get
- {
- if (EndTime - StartTime == 0)
- return 1;
-
- return (float)((TrackTime - StartTime) / length);
- }
- }
-
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@@ -153,47 +98,28 @@ namespace osu.Game.Screens.Play.HUD
base.OnHoverLost(e);
}
- protected override void UpdateValue(float value)
- {
- // Handled in Update
- }
-
protected override void Update()
{
base.Update();
- playfieldBar.Length = (float)Interpolation.Lerp(playfieldBar.Length, NormalizedValue, Math.Clamp(Time.Elapsed / 40, 0, 1));
- catchupBar.Length = (float)Interpolation.Lerp(catchupBar.Length, normalizedReference, Math.Clamp(Time.Elapsed / 40, 0, 1));
+ playfieldBar.Length = (float)Interpolation.Lerp(playfieldBar.Length, Progress, Math.Clamp(Time.Elapsed / 40, 0, 1));
+ audioBar.Length = (float)Interpolation.Lerp(audioBar.Length, AudioProgress, Math.Clamp(Time.Elapsed / 40, 0, 1));
- if (TrackTime < CurrentTime)
- ChangeChildDepth(catchupBar, -1);
+ if (trackTime > AudioTime)
+ ChangeInternalChildDepth(audioBar, -1);
else
- ChangeChildDepth(catchupBar, 0);
+ ChangeInternalChildDepth(audioBar, 1);
- float timeDelta = (float)(Math.Abs(CurrentTime - TrackTime));
+ float timeDelta = (float)Math.Abs(AudioTime - trackTime);
const float colour_transition_threshold = 20000;
- catchupBar.AccentColour = Interpolation.ValueAt(
+ audioBar.AccentColour = Interpolation.ValueAt(
Math.Min(timeDelta, colour_transition_threshold),
mainColour,
catchUpColour,
0, colour_transition_threshold,
Easing.OutQuint);
-
- catchupBar.Alpha = Math.Max(1, catchupBar.Length);
- }
-
- private ScheduledDelegate? scheduledSeek;
-
- protected override void OnUserChange(double value)
- {
- scheduledSeek?.Cancel();
- scheduledSeek = Schedule(() =>
- {
- if (Interactive)
- OnSeek?.Invoke(value);
- });
}
private partial class RoundedBar : Container
diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs
index 48809796f3..f01c11855c 100644
--- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs
+++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs
@@ -98,12 +98,7 @@ namespace osu.Game.Screens.Play.HUD
protected override void UpdateProgress(double progress, bool isIntro)
{
- bar.CurrentTime = GameplayClock.CurrentTime;
-
- if (isIntro)
- graph.Progress = 0;
- else
- graph.Progress = (int)(graph.ColumnCount * progress);
+ graph.Progress = isIntro ? 0 : (int)(graph.ColumnCount * progress);
}
protected override void Update()
diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgressBar.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgressBar.cs
index 0e16067dcc..d5a6a75793 100644
--- a/osu.Game/Screens/Play/HUD/DefaultSongProgressBar.cs
+++ b/osu.Game/Screens/Play/HUD/DefaultSongProgressBar.cs
@@ -7,71 +7,27 @@ using osuTK.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.UserInterface;
using osu.Framework.Utils;
-using osu.Framework.Threading;
namespace osu.Game.Screens.Play.HUD
{
- public partial class DefaultSongProgressBar : SliderBar
+ public partial class DefaultSongProgressBar : SongProgressBar
{
- ///
- /// Action which is invoked when a seek is requested, with the proposed millisecond value for the seek operation.
- ///
- public Action? OnSeek { get; set; }
-
- ///
- /// Whether the progress bar should allow interaction, ie. to perform seek operations.
- ///
- public bool Interactive
- {
- get => showHandle;
- set
- {
- if (value == showHandle)
- return;
-
- showHandle = value;
-
- handleBase.FadeTo(showHandle ? 1 : 0, 200);
- }
- }
-
public Color4 FillColour
{
set => fill.Colour = value;
}
- public double StartTime
- {
- set => CurrentNumber.MinValue = value;
- }
-
- public double EndTime
- {
- set => CurrentNumber.MaxValue = value;
- }
-
- public double CurrentTime
- {
- set => CurrentNumber.Value = value;
- }
-
private readonly Box fill;
private readonly Container handleBase;
private readonly Container handleContainer;
- private bool showHandle;
-
public DefaultSongProgressBar(float barHeight, float handleBarHeight, Vector2 handleSize)
{
- CurrentNumber.MinValue = 0;
- CurrentNumber.MaxValue = 1;
-
RelativeSizeAxes = Axes.X;
Height = barHeight + handleBarHeight + handleSize.Y;
- Children = new Drawable[]
+ InternalChildren = new Drawable[]
{
new Box
{
@@ -130,9 +86,14 @@ namespace osu.Game.Screens.Play.HUD
};
}
- protected override void UpdateValue(float value)
+ public override bool Interactive
{
- // handled in update
+ get => base.Interactive;
+ set
+ {
+ base.Interactive = value;
+ handleBase.FadeTo(value ? 1 : 0, 200);
+ }
}
protected override void Update()
@@ -140,22 +101,10 @@ namespace osu.Game.Screens.Play.HUD
base.Update();
handleBase.Height = Height - handleContainer.Height;
- float newX = (float)Interpolation.Lerp(handleBase.X, NormalizedValue * UsableWidth, Math.Clamp(Time.Elapsed / 40, 0, 1));
+ float newX = (float)Interpolation.Lerp(handleBase.X, AudioProgress * DrawWidth, Math.Clamp(Time.Elapsed / 40, 0, 1));
fill.Width = newX;
handleBase.X = newX;
}
-
- private ScheduledDelegate? scheduledSeek;
-
- protected override void OnUserChange(double value)
- {
- scheduledSeek?.Cancel();
- scheduledSeek = Schedule(() =>
- {
- if (showHandle)
- OnSeek?.Invoke(value);
- });
- }
}
}
diff --git a/osu.Game/Screens/Play/HUD/SongProgress.cs b/osu.Game/Screens/Play/HUD/SongProgress.cs
index 4391193df8..296306ec89 100644
--- a/osu.Game/Screens/Play/HUD/SongProgress.cs
+++ b/osu.Game/Screens/Play/HUD/SongProgress.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -70,7 +71,13 @@ namespace osu.Game.Screens.Play.HUD
protected double LastHitTime { get; private set; }
+ ///
+ /// Called every update frame with current progress information.
+ ///
+ /// Current (visual) progress through the beatmap (0..1).
+ /// If true, progress is (0..1) through the intro.
protected abstract void UpdateProgress(double progress, bool isIntro);
+
protected virtual void UpdateObjects(IEnumerable objects) { }
[BackgroundDependencyLoader]
@@ -96,7 +103,7 @@ namespace osu.Game.Screens.Play.HUD
if (objects == null)
return;
- double currentTime = FrameStableClock.CurrentTime;
+ double currentTime = Math.Min(FrameStableClock.CurrentTime, LastHitTime);
bool isInIntro = currentTime < FirstHitTime;
diff --git a/osu.Game/Screens/Play/HUD/SongProgressBar.cs b/osu.Game/Screens/Play/HUD/SongProgressBar.cs
new file mode 100644
index 0000000000..40c4e587b9
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/SongProgressBar.cs
@@ -0,0 +1,97 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Events;
+using osu.Framework.Threading;
+using osuTK;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ public abstract partial class SongProgressBar : CompositeDrawable
+ {
+ ///
+ /// The current seek position of the audio, on a (0..1) range.
+ /// This is generally the seek target, which will eventually match the gameplay clock when it catches up.
+ ///
+ protected double AudioProgress => length == 0 ? 1 : AudioTime / length;
+
+ ///
+ /// The current (non-frame-stable) audio time.
+ ///
+ protected double AudioTime => Math.Clamp(GameplayClock.CurrentTime - StartTime, 0.0, length);
+
+ [Resolved]
+ protected IGameplayClock GameplayClock { get; private set; } = null!;
+
+ ///
+ /// Action which is invoked when a seek is requested, with the proposed millisecond value for the seek operation.
+ ///
+ public Action? OnSeek { get; set; }
+
+ ///
+ /// Whether the progress bar should allow interaction, ie. to perform seek operations.
+ ///
+ public virtual bool Interactive { get; set; }
+
+ public double StartTime { get; set; }
+
+ public double EndTime { get; set; } = 1.0;
+
+ private double length => EndTime - StartTime;
+
+ private bool handleClick;
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ handleClick = true;
+ return base.OnMouseDown(e);
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ if (handleClick)
+ handleMouseInput(e);
+
+ return true;
+ }
+
+ protected override void OnDrag(DragEvent e)
+ {
+ handleMouseInput(e);
+ }
+
+ protected override bool OnDragStart(DragStartEvent e)
+ {
+ Vector2 posDiff = e.MouseDownPosition - e.MousePosition;
+
+ if (Math.Abs(posDiff.X) < Math.Abs(posDiff.Y))
+ {
+ handleClick = false;
+ return false;
+ }
+
+ handleMouseInput(e);
+ return true;
+ }
+
+ private void handleMouseInput(UIEvent e)
+ {
+ if (!Interactive)
+ return;
+
+ double relativeX = Math.Clamp(ToLocalSpace(e.ScreenSpaceMousePosition).X / DrawWidth, 0, 1);
+ onUserChange(StartTime + (EndTime - StartTime) * relativeX);
+ }
+
+ private ScheduledDelegate? scheduledSeek;
+
+ private void onUserChange(double value)
+ {
+ scheduledSeek?.Cancel();
+ scheduledSeek = Schedule(() => OnSeek?.Invoke(value));
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/SpectatorResultsScreen.cs b/osu.Game/Screens/Play/SpectatorResultsScreen.cs
index 001d3b4bbc..393cbddb34 100644
--- a/osu.Game/Screens/Play/SpectatorResultsScreen.cs
+++ b/osu.Game/Screens/Play/SpectatorResultsScreen.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Screens.Play
private void userBeganPlaying(int userId, SpectatorState state)
{
- if (userId == Score.UserID)
+ if (userId == Score?.UserID)
{
Schedule(() =>
{
diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs
index 22509b2cea..22c1e26d43 100644
--- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs
@@ -5,10 +5,11 @@
using System;
using System.Threading;
+using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Extensions;
using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
@@ -32,7 +33,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
}
[BackgroundDependencyLoader]
- private void load(ScorePerformanceCache performanceCache)
+ private void load(BeatmapDifficultyCache difficultyCache, CancellationToken? cancellationToken)
{
if (score.PP.HasValue)
{
@@ -40,8 +41,19 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
}
else
{
- performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token)
- .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()?.Total)), cancellationTokenSource.Token);
+ Task.Run(async () =>
+ {
+ var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, cancellationToken ?? default).ConfigureAwait(false);
+ var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator();
+
+ // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value.
+ if (attributes?.Attributes == null || performanceCalculator == null)
+ return;
+
+ var result = await performanceCalculator.CalculateAsync(score, attributes.Value.Attributes, cancellationToken ?? default).ConfigureAwait(false);
+
+ Schedule(() => setPerformanceValue(result.Total));
+ }, cancellationToken ?? default);
}
}
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index 697d62ad6e..82dade40eb 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -45,6 +46,7 @@ namespace osu.Game.Screens.Ranking
public readonly Bindable SelectedScore = new Bindable();
+ [CanBeNull]
public readonly ScoreInfo Score;
protected ScorePanelList ScorePanelList { get; private set; }
@@ -69,7 +71,7 @@ namespace osu.Game.Screens.Ranking
private Sample popInSample;
- protected ResultsScreen(ScoreInfo score, bool allowRetry, bool allowWatchingReplay = true)
+ protected ResultsScreen([CanBeNull] ScoreInfo score, bool allowRetry, bool allowWatchingReplay = true)
{
Score = score;
this.allowRetry = allowRetry;
@@ -275,6 +277,11 @@ namespace osu.Game.Screens.Ranking
if (base.OnExiting(e))
return true;
+ // This is a stop-gap safety against components holding references to gameplay after exiting the gameplay flow.
+ // Right now, HitEvents are only used up to the results screen. If this changes in the future we need to remove
+ // HitObject references from HitEvent.
+ Score?.HitEvents.Clear();
+
this.FadeOut(100);
return false;
}
diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
index da08a26a58..22d631e137 100644
--- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -45,12 +46,16 @@ namespace osu.Game.Screens.Ranking
{
base.LoadComplete();
+ Debug.Assert(Score != null);
+
if (ShowUserStatistics)
statisticsSubscription = soloStatisticsWatcher.RegisterForStatisticsUpdateAfter(Score, update => statisticsUpdate.Value = update);
}
protected override StatisticsPanel CreateStatisticsPanel()
{
+ Debug.Assert(Score != null);
+
if (ShowUserStatistics)
{
return new SoloStatisticsPanel(Score)
@@ -64,6 +69,8 @@ namespace osu.Game.Screens.Ranking
protected override APIRequest? FetchScores(Action>? scoresCallback)
{
+ Debug.Assert(Score != null);
+
if (Score.BeatmapInfo!.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending)
return null;
diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs
index ee0ce6183d..8b13f0951c 100644
--- a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs
+++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs
@@ -39,9 +39,6 @@ namespace osu.Game.Screens.Ranking.Statistics
private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
- [Resolved]
- private ScorePerformanceCache performanceCache { get; set; }
-
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; }
@@ -148,7 +145,7 @@ namespace osu.Game.Screens.Ranking.Statistics
spinner.Show();
- new PerformanceBreakdownCalculator(playableBeatmap, difficultyCache, performanceCache)
+ new PerformanceBreakdownCalculator(playableBeatmap, difficultyCache)
.CalculateAsync(score, cancellationTokenSource.Token)
.ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely())));
}
diff --git a/osu.Game/Online/Notifications/NotificationsClient.cs b/osu.Game/Tests/PollingChatClient.cs
similarity index 59%
rename from osu.Game/Online/Notifications/NotificationsClient.cs
rename to osu.Game/Tests/PollingChatClient.cs
index 5762e0e588..eb29b35c1d 100644
--- a/osu.Game/Online/Notifications/NotificationsClient.cs
+++ b/osu.Game/Tests/PollingChatClient.cs
@@ -6,34 +6,39 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Chat;
-namespace osu.Game.Online.Notifications
+namespace osu.Game.Tests
{
- ///
- /// An abstract client which receives notification-related events (chat/notifications).
- ///
- public abstract class NotificationsClient : PersistentEndpointClient
+ public class PollingChatClient : PersistentEndpointClient
{
- public Action? ChannelJoined;
- public Action? ChannelParted;
- public Action>? NewMessages;
- public Action? PresenceReceived;
+ public event Action? ChannelJoined;
+ public event Action>? NewMessages;
+ public event Action? PresenceReceived;
- protected readonly IAPIProvider API;
+ private readonly IAPIProvider api;
private long lastMessageId;
- protected NotificationsClient(IAPIProvider api)
+ public PollingChatClient(IAPIProvider api)
{
- API = api;
+ this.api = api;
}
public override Task ConnectAsync(CancellationToken cancellationToken)
{
- API.Queue(CreateInitialFetchRequest(0));
+ Task.Run(async () =>
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ await api.PerformAsync(CreateInitialFetchRequest()).ConfigureAwait(true);
+ await Task.Delay(1000, cancellationToken).ConfigureAwait(true);
+ }
+ }, cancellationToken);
+
return Task.CompletedTask;
}
@@ -46,11 +51,11 @@ namespace osu.Game.Online.Notifications
if (updates?.Presence != null)
{
foreach (var channel in updates.Presence)
- HandleChannelJoined(channel);
+ handleChannelJoined(channel);
//todo: handle left channels
- HandleMessages(updates.Messages);
+ handleMessages(updates.Messages);
}
PresenceReceived?.Invoke();
@@ -59,15 +64,13 @@ namespace osu.Game.Online.Notifications
return fetchReq;
}
- protected void HandleChannelJoined(Channel channel)
+ private void handleChannelJoined(Channel channel)
{
channel.Joined.Value = true;
ChannelJoined?.Invoke(channel);
}
- protected void HandleChannelParted(Channel channel) => ChannelParted?.Invoke(channel);
-
- protected void HandleMessages(List? messages)
+ private void handleMessages(List? messages)
{
if (messages == null)
return;
diff --git a/osu.Game/Tests/PollingNotificationsClient.cs b/osu.Game/Tests/PollingNotificationsClient.cs
deleted file mode 100644
index 450c763170..0000000000
--- a/osu.Game/Tests/PollingNotificationsClient.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Threading;
-using System.Threading.Tasks;
-using osu.Game.Online.API;
-using osu.Game.Online.Notifications;
-
-namespace osu.Game.Tests
-{
- ///
- /// A notifications client which polls for new messages every second.
- ///
- public class PollingNotificationsClient : NotificationsClient
- {
- public PollingNotificationsClient(IAPIProvider api)
- : base(api)
- {
- }
-
- public override Task ConnectAsync(CancellationToken cancellationToken)
- {
- Task.Run(async () =>
- {
- while (!cancellationToken.IsCancellationRequested)
- {
- await API.PerformAsync(CreateInitialFetchRequest()).ConfigureAwait(true);
- await Task.Delay(1000, cancellationToken).ConfigureAwait(true);
- }
- }, cancellationToken);
-
- return Task.CompletedTask;
- }
- }
-}
diff --git a/osu.Game/Tests/PollingNotificationsClientConnector.cs b/osu.Game/Tests/PollingNotificationsClientConnector.cs
deleted file mode 100644
index 823fc9d157..0000000000
--- a/osu.Game/Tests/PollingNotificationsClientConnector.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Threading;
-using System.Threading.Tasks;
-using osu.Game.Online.API;
-using osu.Game.Online.Notifications;
-
-namespace osu.Game.Tests
-{
- ///
- /// A connector for s that poll for new messages.
- ///
- public class PollingNotificationsClientConnector : NotificationsClientConnector
- {
- public PollingNotificationsClientConnector(IAPIProvider api)
- : base(api)
- {
- }
-
- protected override Task BuildNotificationClientAsync(CancellationToken cancellationToken)
- => Task.FromResult((NotificationsClient)new PollingNotificationsClient(API));
- }
-}
diff --git a/osu.Game/Tests/TestChatClientConnector.cs b/osu.Game/Tests/TestChatClientConnector.cs
new file mode 100644
index 0000000000..40e15b5ef5
--- /dev/null
+++ b/osu.Game/Tests/TestChatClientConnector.cs
@@ -0,0 +1,49 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using osu.Game.Online;
+using osu.Game.Online.API;
+using osu.Game.Online.Chat;
+
+namespace osu.Game.Tests
+{
+ public class TestChatClientConnector : PersistentEndpointClientConnector, IChatClient
+ {
+ public event Action? ChannelJoined;
+
+ public event Action? ChannelParted
+ {
+ add { }
+ remove { }
+ }
+
+ public event Action>? NewMessages;
+ public event Action? PresenceReceived;
+
+ public void RequestPresence()
+ {
+ // don't really need to do anything special if we poll every second anyway.
+ }
+
+ public TestChatClientConnector(IAPIProvider api)
+ : base(api)
+ {
+ Start();
+ }
+
+ protected sealed override Task BuildConnectionAsync(CancellationToken cancellationToken)
+ {
+ var client = new PollingChatClient(API);
+
+ client.ChannelJoined += c => ChannelJoined?.Invoke(c);
+ client.NewMessages += m => NewMessages?.Invoke(m);
+ client.PresenceReceived += () => PresenceReceived?.Invoke();
+
+ return Task.FromResult(client);
+ }
+ }
+}
diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs
index 947305439e..6069fe4fb0 100644
--- a/osu.Game/Tests/Visual/OsuGameTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs
@@ -178,6 +178,7 @@ namespace osu.Game.Tests.Visual
LocalConfig.SetValue(OsuSetting.ShowFirstRunSetup, false);
API.Login("Rhythm Champion", "osu!");
+ ((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
Dependencies.Get().SetValue(Static.MutedAudioNotificationShownOnce, true);
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 1b1abe3971..6f71424130 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,8 +36,8 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 98e8b136e5..bbcabc6360 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -23,6 +23,6 @@
iossimulator-x64
-
+
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/300076680-5cbe0121-ed68-414f-9ddc-dd993ac97e62.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/300076680-5cbe0121-ed68-414f-9ddc-dd993ac97e62.png
new file mode 100644
index 0000000000..21f5f0f3a0
Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/300076680-5cbe0121-ed68-414f-9ddc-dd993ac97e62.png differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json
index af4b103867..29df54b400 100644
--- a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1 +1,14 @@
-{"images":[{"size":"20x20","idiom":"iphone","filename":"iPhoneNotification2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"iPhoneNotification3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"iPhoneSettings2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"iPhoneSettings3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"iPhoneSpotlight2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"iPhoneSpotlight3x.png","scale":"3x"},{"size":"60x60","idiom":"iphone","filename":"iPhoneApp2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"iPhoneApp3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"iPadNotification1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"iPadNotification2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"iPadSettings1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"iPadSettings2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"iPadSpotlight1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"iPadSpotlight2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"iPadApp1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"iPadApp2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"iPadProApp2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"iOSAppStore.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
\ No newline at end of file
+{
+ "images" : [
+ {
+ "filename" : "300076680-5cbe0121-ed68-414f-9ddc-dd993ac97e62.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iOSAppStore.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iOSAppStore.png
deleted file mode 100644
index 0e8bb029bc..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iOSAppStore.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadApp1x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadApp1x.png
deleted file mode 100644
index 42fead2364..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadApp1x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadApp2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadApp2x.png
deleted file mode 100644
index 785db50cb2..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadApp2x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadNotification1x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadNotification1x.png
deleted file mode 100644
index 8c483a0a7a..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadNotification1x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadNotification2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadNotification2x.png
deleted file mode 100644
index a45b01b91c..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadNotification2x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadProApp2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadProApp2x.png
deleted file mode 100644
index d2ba8f3a7e..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadProApp2x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSettings1x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSettings1x.png
deleted file mode 100644
index 43d577040e..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSettings1x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSettings2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSettings2x.png
deleted file mode 100644
index 1ebec1390b..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSettings2x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSpotlight1x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSpotlight1x.png
deleted file mode 100644
index a45b01b91c..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSpotlight1x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSpotlight2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSpotlight2x.png
deleted file mode 100644
index 717603dd68..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSpotlight2x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneApp2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneApp2x.png
deleted file mode 100644
index 6b61c09db5..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneApp2x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneApp3x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneApp3x.png
deleted file mode 100644
index 78ef8d12b7..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneApp3x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneNotification2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneNotification2x.png
deleted file mode 100644
index a45b01b91c..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneNotification2x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneNotification3x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneNotification3x.png
deleted file mode 100644
index 46ddf1179d..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneNotification3x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSettings2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSettings2x.png
deleted file mode 100644
index 1ebec1390b..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSettings2x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSettings3x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSettings3x.png
deleted file mode 100644
index a8145f0246..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSettings3x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSpotlight2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSpotlight2x.png
deleted file mode 100644
index 717603dd68..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSpotlight2x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSpotlight3x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSpotlight3x.png
deleted file mode 100644
index 6b61c09db5..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSpotlight3x.png and /dev/null differ
diff --git a/osu.iOS/iTunesArtwork b/osu.iOS/iTunesArtwork
deleted file mode 100644
index 1939459992..0000000000
Binary files a/osu.iOS/iTunesArtwork and /dev/null differ
diff --git a/osu.iOS/iTunesArtwork@2x b/osu.iOS/iTunesArtwork@2x
deleted file mode 100644
index 0e8bb029bc..0000000000
Binary files a/osu.iOS/iTunesArtwork@2x and /dev/null differ