From 85e303ec54cd60b75f6b3cc4d0a457caafda4afc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Nov 2023 20:00:09 +0900 Subject: [PATCH 01/93] Add two factor step to api state flow --- osu.Game/Online/API/APIAccess.cs | 5 +++++ osu.Game/Online/API/DummyAPIAccess.cs | 11 ++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 4f586c8fff..a3ceb14a40 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -540,6 +540,11 @@ namespace osu.Game.Online.API /// Failing, + /// + /// Waiting on second factor authentication. + /// + RequiresAuthentication, + /// /// 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 d585124db6..4cf2152193 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -55,6 +55,7 @@ namespace osu.Game.Online.API private bool shouldFailNextLogin; private bool stayConnectingNextLogin; + private bool requiresTwoFactor; /// /// The current connectivity state of the API. @@ -115,7 +116,13 @@ namespace osu.Game.Online.API Id = DUMMY_USER_ID, }; - state.Value = APIState.Online; + if (requiresTwoFactor) + { + state.Value = APIState.RequiresAuthentication; + requiresTwoFactor = false; + } + else + state.Value = APIState.Online; } public void Logout() @@ -142,6 +149,8 @@ namespace osu.Game.Online.API IBindableList IAPIProvider.Friends => Friends; IBindable IAPIProvider.Activity => Activity; + public void RequireTwoFactor() => requiresTwoFactor = true; + /// /// During the next simulated login, the process will fail immediately. /// From 80c879e5ebed1d1d15b8d44de47f1b15ea77470c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Nov 2023 16:37:12 +0900 Subject: [PATCH 02/93] Adjust member ordering in `LoginForm` --- osu.Game/Overlays/Login/LoginForm.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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; From e9d4cf2e24de488c75b9cd0a52e2b524dfca5248 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Nov 2023 20:00:22 +0900 Subject: [PATCH 03/93] Setup basic form flow --- .../Visual/Menus/TestSceneLoginOverlay.cs | 13 +++++++++++++ osu.Game/Overlays/Login/LoginForm.cs | 5 +++++ osu.Game/Overlays/Login/LoginPanel.cs | 6 +++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs index 0bc71924ce..30c0f6644e 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs @@ -36,6 +36,19 @@ namespace osu.Game.Tests.Visual.Menus AddStep("show login overlay", () => loginOverlay.Show()); } + [Test] + public void TestLoginTwoFactorSuccess() + { + AddStep("logout", () => + { + API.Logout(); + ((DummyAPIAccess)API).RequireTwoFactor(); + }); + + AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password"); + AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); + } + [Test] public void TestLoginSuccess() { diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index 80dfca93d2..291e92883b 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -21,6 +21,11 @@ using osu.Game.Localisation; namespace osu.Game.Overlays.Login { + public partial class AccountVerificationForm : FillFlowContainer + { + + } + public partial class LoginForm : FillFlowContainer { private TextBox username = null!; diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 71ecf2e75a..59b9c6ab9f 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.RequiresAuthentication: + Child = form = new AccountVerificationForm(); + break; + case APIState.Failing: case APIState.Connecting: LinkFlowContainer linkFlow; From c4e461ba447eea892668e12967af659e17689b1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Nov 2023 11:49:49 +0900 Subject: [PATCH 04/93] Add two factor auth form --- .../Overlays/Login/AccountVerificationForm.cs | 92 +++++++++++++++++++ osu.Game/Overlays/Login/LoginForm.cs | 5 - 2 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Overlays/Login/AccountVerificationForm.cs diff --git a/osu.Game/Overlays/Login/AccountVerificationForm.cs b/osu.Game/Overlays/Login/AccountVerificationForm.cs new file mode 100644 index 0000000000..a27cc01ed2 --- /dev/null +++ b/osu.Game/Overlays/Login/AccountVerificationForm.cs @@ -0,0 +1,92 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; +using osuTK; + +namespace osu.Game.Overlays.Login +{ + public partial class AccountVerificationForm : FillFlowContainer + { + private OsuTextBox code = null!; + private LinkFlowContainer explainText = null!; + + [BackgroundDependencyLoader] + private void load() + { + 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.", + }, + code = 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, + }, + 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, + }, + }; + + explainText.AddParagraph("Make sure to check your spam folder if you can't find the email."); + explainText.AddParagraph("If you can't access your email or have forgotten what you used, please follow the "); + explainText.AddLink("email recovery process here", () => { }); + explainText.AddText(". You can also "); + explainText.AddLink("request another code", () => { }); + explainText.AddText(" or "); + explainText.AddLink("sign out", () => { }); + explainText.AddText("."); + } + + public override bool AcceptsFocus => true; + + protected override bool OnClick(ClickEvent e) => true; + + protected override void OnFocus(FocusEvent e) + { + Schedule(() => { GetContainingInputManager().ChangeFocus(code); }); + } + } +} diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index 291e92883b..80dfca93d2 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -21,11 +21,6 @@ using osu.Game.Localisation; namespace osu.Game.Overlays.Login { - public partial class AccountVerificationForm : FillFlowContainer - { - - } - public partial class LoginForm : FillFlowContainer { private TextBox username = null!; From 0e4244a692227d6f08a6ec2a928ee3dcec69d4ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Nov 2023 16:38:07 +0900 Subject: [PATCH 05/93] Add `APIAccess` flow for 2fa --- osu.Game/Online/API/APIAccess.cs | 40 +++++++++++++++++++++++++-- osu.Game/Online/API/DummyAPIAccess.cs | 7 ++++- osu.Game/Online/API/IAPIProvider.cs | 6 ++++ osu.Game/Overlays/Login/LoginPanel.cs | 2 +- 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index a3ceb14a40..f9dd97a8f2 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -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; @@ -183,6 +185,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) @@ -190,8 +193,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. @@ -208,11 +209,14 @@ namespace osu.Game.Online.API if (!authentication.HasValidAccessToken) { + state.Value = APIState.Connecting; LastLoginError = null; try { authentication.AuthenticateWithLogin(ProvidedUsername, password); + state.Value = APIState.RequiresSecondFactorAuth; + return; } catch (Exception e) { @@ -225,6 +229,28 @@ namespace osu.Game.Online.API } } + if (state.Value == APIState.RequiresSecondFactorAuth) + { + if (string.IsNullOrEmpty(SecondFactorCode)) + return; + + state.Value = APIState.Connecting; + LastLoginError = null; + + // TODO: use code to ensure second factor authentication completed. + Thread.Sleep(1000); + bool success = SecondFactorCode == "00000000"; + SecondFactorCode = null; + + if (!success) + { + state.Value = APIState.RequiresSecondFactorAuth; + LastLoginError = new InvalidOperationException("Second factor auth failed"); + SecondFactorCode = null; + return; + } + } + var userReq = new GetUserRequest(); userReq.Failure += ex => { @@ -307,6 +333,13 @@ 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); @@ -493,6 +526,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 @@ -543,7 +577,7 @@ namespace osu.Game.Online.API /// /// Waiting on second factor authentication. /// - RequiresAuthentication, + 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 4cf2152193..bd833d39dd 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -118,13 +118,18 @@ namespace osu.Game.Online.API if (requiresTwoFactor) { - state.Value = APIState.RequiresAuthentication; + state.Value = APIState.RequiresSecondFactorAuth; requiresTwoFactor = false; } else state.Value = APIState.Online; } + public void AuthenticateSecondFactor(string code) + { + state.Value = APIState.Online; + } + public void Logout() { state.Value = APIState.Offline; diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index a1d7006c8c..5f99ad2f4d 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -106,6 +106,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. /// diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 59b9c6ab9f..f896d03231 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -81,7 +81,7 @@ namespace osu.Game.Overlays.Login }; break; - case APIState.RequiresAuthentication: + case APIState.RequiresSecondFactorAuth: Child = form = new AccountVerificationForm(); break; From 285f740e2a2aa3754445141fa2e974fa4b1d9cd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Nov 2023 16:53:37 +0900 Subject: [PATCH 06/93] Update various components to handle new state --- osu.Game/Overlays/Login/LoginPanel.cs | 2 +- ...icationForm.cs => SecondFactorAuthForm.cs} | 32 +++++++++++++++---- .../Overlays/Toolbar/ToolbarUserButton.cs | 1 + 3 files changed, 28 insertions(+), 7 deletions(-) rename osu.Game/Overlays/Login/{AccountVerificationForm.cs => SecondFactorAuthForm.cs} (78%) diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index f896d03231..ce0b0a5a48 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -82,7 +82,7 @@ namespace osu.Game.Overlays.Login break; case APIState.RequiresSecondFactorAuth: - Child = form = new AccountVerificationForm(); + Child = form = new SecondFactorAuthForm(); break; case APIState.Failing: diff --git a/osu.Game/Overlays/Login/AccountVerificationForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs similarity index 78% rename from osu.Game/Overlays/Login/AccountVerificationForm.cs rename to osu.Game/Overlays/Login/SecondFactorAuthForm.cs index a27cc01ed2..cfec46c599 100644 --- a/osu.Game/Overlays/Login/AccountVerificationForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs @@ -8,15 +8,20 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Overlays.Settings; using osuTK; namespace osu.Game.Overlays.Login { - public partial class AccountVerificationForm : FillFlowContainer + public partial class SecondFactorAuthForm : FillFlowContainer { - private OsuTextBox code = null!; + private OsuTextBox codeTextBox = null!; private LinkFlowContainer explainText = null!; + private ErrorTextFlowContainer errorText = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; [BackgroundDependencyLoader] private void load() @@ -43,18 +48,18 @@ namespace osu.Game.Overlays.Login AutoSizeAxes = Axes.Y, Text = "An email has been sent to you with a verification code. Enter the code.", }, - code = new OsuTextBox + codeTextBox = new OsuTextBox { PlaceholderText = "Enter code", RelativeSizeAxes = Axes.X, - TabbableContentContainer = this + TabbableContentContainer = this, }, explainText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular)) { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, }, - new ErrorTextFlowContainer + errorText = new ErrorTextFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -78,6 +83,21 @@ namespace osu.Game.Overlays.Login explainText.AddText(" or "); explainText.AddLink("sign out", () => { }); 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; @@ -86,7 +106,7 @@ namespace osu.Game.Overlays.Login protected override void OnFocus(FocusEvent e) { - Schedule(() => { GetContainingInputManager().ChangeFocus(code); }); + Schedule(() => { GetContainingInputManager().ChangeFocus(codeTextBox); }); } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index 028decea1e..f0b5aed3cf 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -101,6 +101,7 @@ namespace osu.Game.Overlays.Toolbar switch (state.NewValue) { + case APIState.RequiresSecondFactorAuth: case APIState.Connecting: TooltipText = ToolbarStrings.Connecting; spinner.Show(); From f7fa9c90d66c9ba05f4bc50c1bc78274caa2c8cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Nov 2023 18:16:02 +0900 Subject: [PATCH 07/93] Add test coverage of 2FA flow --- .../Visual/Menus/TestSceneLoginOverlay.cs | 24 +++++++++---------- osu.Game/Online/API/DummyAPIAccess.cs | 13 ++++++---- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs index 30c0f6644e..0c5f5a321c 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs @@ -10,6 +10,7 @@ using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Overlays; +using osu.Game.Overlays.Login; using osu.Game.Users.Drawables; using osuTK.Input; @@ -36,28 +37,25 @@ namespace osu.Game.Tests.Visual.Menus AddStep("show login overlay", () => loginOverlay.Show()); } - [Test] - public void TestLoginTwoFactorSuccess() - { - AddStep("logout", () => - { - API.Logout(); - ((DummyAPIAccess)API).RequireTwoFactor(); - }); - - AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password"); - AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); - } - [Test] 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("enter code", () => loginOverlay.ChildrenOfType().First().Text = "88800088"); + assertAPIState(APIState.Online); } + private void assertAPIState(APIState expected) => + AddUntilStep($"login state is {expected}", () => API.State.Value, () => Is.EqualTo(expected)); + [Test] public void TestLoginFailure() { diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index bd833d39dd..bd462ac23e 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -55,7 +55,7 @@ namespace osu.Game.Online.API private bool shouldFailNextLogin; private bool stayConnectingNextLogin; - private bool requiresTwoFactor; + private bool requiredSecondFactorAuth = true; /// /// The current connectivity state of the API. @@ -116,13 +116,15 @@ namespace osu.Game.Online.API Id = DUMMY_USER_ID, }; - if (requiresTwoFactor) + if (requiredSecondFactorAuth) { state.Value = APIState.RequiresSecondFactorAuth; - requiresTwoFactor = false; } else + { state.Value = APIState.Online; + requiredSecondFactorAuth = true; + } } public void AuthenticateSecondFactor(string code) @@ -154,7 +156,10 @@ namespace osu.Game.Online.API IBindableList IAPIProvider.Friends => Friends; IBindable IAPIProvider.Activity => Activity; - public void RequireTwoFactor() => requiresTwoFactor = true; + /// + /// Skip 2FA requirement for next login. + /// + public void SkipSecondFactor() => requiredSecondFactorAuth = false; /// /// During the next simulated login, the process will fail immediately. From 167f5b4ef401317b3721187147ec7a591288e092 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Nov 2023 18:21:06 +0900 Subject: [PATCH 08/93] Tidy up localisations and connect missing links --- osu.Game/Overlays/Login/SecondFactorAuthForm.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs index cfec46c599..60c04eedee 100644 --- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs @@ -10,6 +10,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Overlays.Settings; +using osu.Game.Resources.Localisation.Web; using osuTK; namespace osu.Game.Overlays.Login @@ -75,13 +76,17 @@ namespace osu.Game.Overlays.Login }, }; - explainText.AddParagraph("Make sure to check your spam folder if you can't find the email."); + 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("email recovery process here", () => { }); + explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.WebsiteRootUrl}/home/password-reset"); explainText.AddText(". You can also "); - explainText.AddLink("request another code", () => { }); + explainText.AddLink(UserVerificationStrings.BoxInfoReissueLink, () => + { + // TODO: request another code. + }); explainText.AddText(" or "); - explainText.AddLink("sign out", () => { }); + explainText.AddLink(UserVerificationStrings.BoxInfoLogoutLink, () => { api.Logout(); }); explainText.AddText("."); codeTextBox.Current.BindValueChanged(code => From 7472dc9bb5e1e410023ab98d778c5380d1d2f958 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Nov 2023 20:39:23 +0900 Subject: [PATCH 09/93] Update `APIState` checks --- osu.Game/Online/OnlineViewContainer.cs | 5 +++++ osu.Game/Overlays/AccountCreationOverlay.cs | 5 +++++ 2 files changed, 10 insertions(+) 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/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(); } } } From c9945b41c1007ab2ff18900d1d70686598e57b9d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jan 2024 18:36:25 +0900 Subject: [PATCH 10/93] Remove rounding of slider velocity (when applied as scroll speed) This affects both osu!taiko and osu!mania. Closes https://github.com/ppy/osu/issues/25862. --- osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index 7edf892f35..0138ac7569 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -21,7 +21,6 @@ namespace osu.Game.Beatmaps.ControlPoints /// public readonly BindableDouble ScrollSpeedBindable = new BindableDouble(1) { - Precision = 0.01, MinValue = 0.01, MaxValue = 10 }; From d0d09a86578854c30e802083dd4d977a967db1e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Jan 2024 16:55:00 +0100 Subject: [PATCH 11/93] Fix login overlay test not clearing second auth factor --- osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs index 0c5f5a321c..0c0edca995 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs @@ -89,6 +89,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()); From d7e957087ebb348df86692c7ab525f27df8bf4b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Jan 2024 16:56:39 +0100 Subject: [PATCH 12/93] Fix account creation overlay test not clearing second auth factor --- .../Visual/Online/TestSceneAccountCreationOverlay.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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); } } From 5d26afc53175678fc241379dcde7d286b72e91c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Jan 2024 17:00:07 +0100 Subject: [PATCH 13/93] Fix `OsuGameTestScene` not clearing second auth factor --- osu.Game/Tests/Visual/OsuGameTestScene.cs | 1 + 1 file changed, 1 insertion(+) 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); From 0d7834af5fc5f4f8dc7f67903cced787b39416df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Jan 2024 18:04:41 +0100 Subject: [PATCH 14/93] Ensure all remaining test usages of `IAPIAccess.Login()` also authenticate with second factor --- .../Gameplay/TestScenePlayerLocalScoreImport.cs | 6 +++++- .../Visual/Menus/TestSceneToolbarUserButton.cs | 12 ++++++++---- .../Visual/Online/TestSceneCommentActions.cs | 1 + .../Visual/Online/TestSceneFavouriteButton.cs | 13 +++++++++++-- .../Visual/Online/TestSceneSoloStatisticsWatcher.cs | 6 +++++- .../Visual/Online/TestSceneUserProfileOverlay.cs | 12 ++++++++++-- osu.Game.Tests/Visual/Online/TestSceneVotePill.cs | 7 ++++++- .../Visual/UserInterface/TestSceneCommentEditor.cs | 6 +++++- 8 files changed, 51 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs index fafd1330cc..22422a685e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs @@ -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/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/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..bb78cf9fd4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs @@ -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/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(); } From 7c140408ea20fdc8122fabe92fdff93501d16d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 24 Jan 2024 13:53:40 +0100 Subject: [PATCH 15/93] Add request structures for verification endpoints --- .../ReissueVerificationCodeRequest.cs | 22 ++++++++++++++ .../API/Requests/VerifySessionRequest.cs | 30 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 osu.Game/Online/API/Requests/ReissueVerificationCodeRequest.cs create mode 100644 osu.Game/Online/API/Requests/VerifySessionRequest.cs 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/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"; + } +} From 7b4721565702a71aa35e4ede372b3468bd72fb52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 24 Jan 2024 14:22:57 +0100 Subject: [PATCH 16/93] Split `/me` request from `/users` requests Them being together always bothered me and led to the abject failure that is `APIUser` and its sprawl. Now that I'm about to add a flag that is unique to `/me` for verification purposes, I'm not repeating the errors of the past by adding yet another flag to `APIUser` that is never present outside of a single usage context. --- osu.Game/Online/API/APIAccess.cs | 2 +- osu.Game/Online/API/Requests/GetMeRequest.cs | 24 +++++++++++++++++++ .../Online/API/Requests/GetUserRequest.cs | 19 ++++----------- .../Online/API/Requests/Responses/APIMe.cs | 9 +++++++ 4 files changed, 39 insertions(+), 15 deletions(-) create mode 100644 osu.Game/Online/API/Requests/GetMeRequest.cs create mode 100644 osu.Game/Online/API/Requests/Responses/APIMe.cs diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 51472dab30..42cf39414e 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -266,7 +266,7 @@ namespace osu.Game.Online.API } } - var userReq = new GetUserRequest(); + var userReq = new GetMeRequest(); userReq.Failure += ex => { if (ex is APIException) 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/Responses/APIMe.cs b/osu.Game/Online/API/Requests/Responses/APIMe.cs new file mode 100644 index 0000000000..e4c4e8ce4f --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIMe.cs @@ -0,0 +1,9 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIMe : APIUser + { + } +} From ddc2bbeb9bd6ca5fef3050ff690b53e2ac01cd9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 24 Jan 2024 14:24:33 +0100 Subject: [PATCH 17/93] Add `session_verified` attribute to `/me` response --- osu.Game/Online/API/Requests/Responses/APIMe.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Online/API/Requests/Responses/APIMe.cs b/osu.Game/Online/API/Requests/Responses/APIMe.cs index e4c4e8ce4f..3cbddbe5e7 100644 --- a/osu.Game/Online/API/Requests/Responses/APIMe.cs +++ b/osu.Game/Online/API/Requests/Responses/APIMe.cs @@ -1,9 +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; } } } From 445a7450e015458aaf071643ba024ca55da57ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 24 Jan 2024 15:57:40 +0100 Subject: [PATCH 18/93] Implement verification from within client --- osu.Game/Online/API/APIAccess.cs | 61 +++++++++++++++++--------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 42cf39414e..389816fcf8 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -230,8 +230,6 @@ namespace osu.Game.Online.API try { authentication.AuthenticateWithLogin(ProvidedUsername, password); - state.Value = APIState.RequiresSecondFactorAuth; - return; } catch (Exception e) { @@ -244,28 +242,6 @@ namespace osu.Game.Online.API } } - if (state.Value == APIState.RequiresSecondFactorAuth) - { - if (string.IsNullOrEmpty(SecondFactorCode)) - return; - - state.Value = APIState.Connecting; - LastLoginError = null; - - // TODO: use code to ensure second factor authentication completed. - Thread.Sleep(1000); - bool success = SecondFactorCode == "00000000"; - SecondFactorCode = null; - - if (!success) - { - state.Value = APIState.RequiresSecondFactorAuth; - LastLoginError = new InvalidOperationException("Second factor auth failed"); - SecondFactorCode = null; - return; - } - } - var userReq = new GetMeRequest(); userReq.Failure += ex => { @@ -285,14 +261,13 @@ namespace osu.Game.Online.API state.Value = APIState.Failing; } }; - userReq.Success += user => + userReq.Success += me => { - user.Status.Value = configStatus.Value ?? UserStatus.Online; + me.Status.Value = configStatus.Value ?? UserStatus.Online; - setLocalUser(user); + setLocalUser(me); - // we're connected! - state.Value = APIState.Online; + state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth; failureCount = 0; }; @@ -302,6 +277,34 @@ namespace osu.Game.Online.API return; } + if (state.Value == APIState.RequiresSecondFactorAuth) + { + 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; + } + var friendsReq = new GetFriendsRequest(); friendsReq.Failure += _ => state.Value = APIState.Failing; friendsReq.Success += res => From 602c3bc2d90e57f84c6cf40e64eb6df1e168d945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 24 Jan 2024 16:17:13 +0100 Subject: [PATCH 19/93] Hook up reissue request --- osu.Game/Overlays/Login/SecondFactorAuthForm.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs index 60c04eedee..566587a541 100644 --- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs @@ -5,10 +5,12 @@ 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; @@ -83,7 +85,9 @@ namespace osu.Game.Overlays.Login explainText.AddText(". You can also "); explainText.AddLink(UserVerificationStrings.BoxInfoReissueLink, () => { - // TODO: request another code. + var reissueRequest = new ReissueVerificationCodeRequest(); + reissueRequest.Failure += ex => Logger.Error(ex, @"Failed to retrieve new verification code."); + api.Perform(reissueRequest); }); explainText.AddText(" or "); explainText.AddLink(UserVerificationStrings.BoxInfoLogoutLink, () => { api.Logout(); }); From 62a0c236bc2eb706bb1f2840e80f7ed7df9c3b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 24 Jan 2024 20:58:23 +0100 Subject: [PATCH 20/93] Split out raw websocket logic from conjoined notifications client contrivance --- .../WebSocket/OsuClientWebSocket.cs | 154 ++++++++++++++++++ .../WebSocket/WebSocketNotificationsClient.cs | 94 +---------- .../WebSocketNotificationsClientConnector.cs | 11 +- 3 files changed, 163 insertions(+), 96 deletions(-) create mode 100644 osu.Game/Online/Notifications/WebSocket/OsuClientWebSocket.cs diff --git a/osu.Game/Online/Notifications/WebSocket/OsuClientWebSocket.cs b/osu.Game/Online/Notifications/WebSocket/OsuClientWebSocket.cs new file mode 100644 index 0000000000..965f606bdc --- /dev/null +++ b/osu.Game/Online/Notifications/WebSocket/OsuClientWebSocket.cs @@ -0,0 +1,154 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using System.Net; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using osu.Framework.Extensions.TypeExtensions; +using osu.Framework.Logging; +using osu.Game.Online.API; + +namespace osu.Game.Online.Notifications.WebSocket +{ + public class OsuClientWebSocket : IAsyncDisposable + { + public event Func? MessageReceived; + public event Func? Closed; + + private readonly string endpoint; + private readonly ClientWebSocket socket; + + private CancellationTokenSource? linkedTokenSource = null; + + public OsuClientWebSocket(IAPIProvider api, string endpoint) + { + socket = new ClientWebSocket(); + socket.Options.SetRequestHeader(@"Authorization", @$"Bearer {api.AccessToken}"); + socket.Options.Proxy = WebRequest.DefaultWebProxy; + if (socket.Options.Proxy != null) + socket.Options.Proxy.Credentials = CredentialCache.DefaultCredentials; + + this.endpoint = endpoint; + } + + public async Task ConnectAsync(CancellationToken cancellationToken) + { + if (socket.State == WebSocketState.Connecting || socket.State == WebSocketState.Open) + throw new InvalidOperationException("Connection is already opened"); + + Debug.Assert(linkedTokenSource == null); + linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + await socket.ConnectAsync(new Uri(endpoint), linkedTokenSource.Token).ConfigureAwait(false); + runReadLoop(linkedTokenSource.Token); + } + + private void runReadLoop(CancellationToken cancellationToken) => Task.Factory.StartNew(async () => + { + byte[] buffer = new byte[1024]; + StringBuilder messageResult = new StringBuilder(); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false); + + switch (result.MessageType) + { + case WebSocketMessageType.Text: + messageResult.Append(Encoding.UTF8.GetString(buffer[..result.Count])); + + if (result.EndOfMessage) + { + SocketMessage? message = JsonConvert.DeserializeObject(messageResult.ToString()); + messageResult.Clear(); + + Debug.Assert(message != null); + + if (message.Error != null) + { + Logger.Log($"{GetType().ReadableName()} error: {message.Error}", LoggingTarget.Network); + break; + } + + await invokeMessageReceived(message).ConfigureAwait(false); + } + + break; + + case WebSocketMessageType.Binary: + throw new NotImplementedException("Binary message type not supported."); + + case WebSocketMessageType.Close: + throw new WebException("Connection closed by remote host."); + } + } + catch (Exception ex) + { + await invokeClosed(ex).ConfigureAwait(false); + return; + } + } + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default); + + private async Task invokeMessageReceived(SocketMessage message) + { + if (MessageReceived == null) + return; + + var invocationList = MessageReceived.GetInvocationList(); + + // ReSharper disable once PossibleInvalidCastExceptionInForeachLoop + foreach (Func handler in invocationList) + await handler.Invoke(message).ConfigureAwait(false); + } + + private async Task invokeClosed(Exception ex) + { + if (Closed == null) + return; + + var invocationList = Closed.GetInvocationList(); + + // ReSharper disable once PossibleInvalidCastExceptionInForeachLoop + foreach (Func handler in invocationList) + await handler.Invoke(ex).ConfigureAwait(false); + } + + public Task SendMessage(SocketMessage message, CancellationToken cancellationToken) + { + if (socket.State != WebSocketState.Open) + return Task.CompletedTask; + + return socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken); + } + + public async Task DisconnectAsync() + { + linkedTokenSource?.Cancel(); + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, @"Disconnecting", CancellationToken.None).ConfigureAwait(false); + linkedTokenSource?.Dispose(); + linkedTokenSource = null; + } + + public async ValueTask DisposeAsync() + { + try + { + await DisconnectAsync().ConfigureAwait(false); + } + catch + { + // Closure can fail if the connection is aborted. Don't really care since it's disposed anyway. + } + + socket.Dispose(); + } + } +} diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs index 73e5dcec6f..b85a6ca3fe 100644 --- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs +++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs @@ -1,17 +1,11 @@ // 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.Diagnostics; -using System.Net; -using System.Net.WebSockets; -using System.Text; using System.Threading; 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; @@ -23,96 +17,25 @@ namespace osu.Game.Online.Notifications.WebSocket /// public class WebSocketNotificationsClient : NotificationsClient { - private readonly ClientWebSocket socket; - private readonly string endpoint; + private readonly OsuClientWebSocket socket; private readonly ConcurrentDictionary channelsMap = new ConcurrentDictionary(); - public WebSocketNotificationsClient(ClientWebSocket socket, string endpoint, IAPIProvider api) + public WebSocketNotificationsClient(IAPIProvider api, string endpoint) : base(api) { - this.socket = socket; - this.endpoint = endpoint; + socket = new OsuClientWebSocket(api, endpoint); + socket.MessageReceived += onMessageReceivedAsync; + socket.Closed += InvokeClosed; } 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 socket.ConnectAsync(cancellationToken).ConfigureAwait(false); + await socket.SendMessage(new StartChatRequest(), CancellationToken.None).ConfigureAwait(false); await base.ConnectAsync(cancellationToken).ConfigureAwait(false); } - private void runReadLoop(CancellationToken cancellationToken) => Task.Run(async () => - { - byte[] buffer = new byte[1024]; - StringBuilder messageResult = new StringBuilder(); - - while (!cancellationToken.IsCancellationRequested) - { - try - { - WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false); - - switch (result.MessageType) - { - case WebSocketMessageType.Text: - messageResult.Append(Encoding.UTF8.GetString(buffer[..result.Count])); - - if (result.EndOfMessage) - { - SocketMessage? message = JsonConvert.DeserializeObject(messageResult.ToString()); - messageResult.Clear(); - - Debug.Assert(message != null); - - if (message.Error != null) - { - Logger.Log($"{GetType().ReadableName()} error: {message.Error}", LoggingTarget.Network); - break; - } - - await onMessageReceivedAsync(message).ConfigureAwait(false); - } - - break; - - case WebSocketMessageType.Binary: - throw new NotImplementedException("Binary message type not supported."); - - case WebSocketMessageType.Close: - throw new WebException("Connection closed by remote host."); - } - } - catch (Exception ex) - { - await InvokeClosed(ex).ConfigureAwait(false); - return; - } - } - }, cancellationToken); - - private async Task closeAsync() - { - try - { - await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, @"Disconnecting", CancellationToken.None).ConfigureAwait(false); - } - catch - { - // Closure can fail if the connection is aborted. Don't really care since it's disposed anyway. - } - } - - private async Task sendMessage(SocketMessage message, CancellationToken cancellationToken) - { - 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) @@ -173,8 +96,7 @@ namespace osu.Game.Online.Notifications.WebSocket public override async ValueTask DisposeAsync() { await base.DisposeAsync().ConfigureAwait(false); - await closeAsync().ConfigureAwait(false); - socket.Dispose(); + await socket.DisposeAsync().ConfigureAwait(false); } } } diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs index f50369a06c..6c61281e90 100644 --- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs +++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.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. -using System.Net; -using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using osu.Game.Online.API; @@ -33,14 +31,7 @@ namespace osu.Game.Online.Notifications.WebSocket api.Queue(req); string endpoint = await tcs.Task.ConfigureAwait(false); - - ClientWebSocket socket = new ClientWebSocket(); - socket.Options.SetRequestHeader(@"Authorization", @$"Bearer {api.AccessToken}"); - socket.Options.Proxy = WebRequest.DefaultWebProxy; - if (socket.Options.Proxy != null) - socket.Options.Proxy.Credentials = CredentialCache.DefaultCredentials; - - return new WebSocketNotificationsClient(socket, endpoint, api); + return new WebSocketNotificationsClient(api, endpoint); } } } From e3eb7a8b4281c7d3b2170b10b2681cde8ee7f066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 24 Jan 2024 21:33:34 +0100 Subject: [PATCH 21/93] Support verification via clicking link from e-mail --- osu.Game/Online/API/APIAccess.cs | 50 ++++++++++++++++++- .../DevelopmentEndpointConfiguration.cs | 1 + osu.Game/Online/EndpointConfiguration.cs | 5 ++ .../ExperimentalEndpointConfiguration.cs | 1 + .../Online/ProductionEndpointConfiguration.cs | 1 + 5 files changed, 57 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 389816fcf8..dabb2cc94c 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -11,8 +11,10 @@ using System.Net.Http; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using Newtonsoft.Json.Linq; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -76,6 +78,11 @@ namespace osu.Game.Online.API private readonly Logger log; + private string webSocketEndpointUrl; + + [CanBeNull] + private OsuClientWebSocket webSocket; + public APIAccess(OsuGameBase game, OsuConfigManager config, EndpointConfiguration endpointConfiguration, string versionHash) { this.game = game; @@ -84,6 +91,7 @@ namespace osu.Game.Online.API APIEndpointUrl = endpointConfiguration.APIEndpointUrl; WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl; + webSocketEndpointUrl = endpointConfiguration.NotificationsWebSocketEndpointUrl; authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl); log = Logger.GetLogger(LoggingTarget.Network); @@ -267,7 +275,10 @@ namespace osu.Game.Online.API setLocalUser(me); - state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth; + if (me.SessionVerified) + state.Value = APIState.Online; + else + setUpSecondFactorAuthentication(); failureCount = 0; }; @@ -350,6 +361,42 @@ namespace osu.Game.Online.API this.password = password; } + private void setUpSecondFactorAuthentication() + { + if (state.Value == APIState.RequiresSecondFactorAuth) + return; + + state.Value = APIState.RequiresSecondFactorAuth; + + try + { + webSocket?.DisposeAsync().AsTask().WaitSafely(); + var newSocket = new OsuClientWebSocket(this, webSocketEndpointUrl); + newSocket.MessageReceived += async msg => + { + if (msg.Event == @"verified") + { + state.Value = APIState.Online; + await newSocket.DisposeAsync().ConfigureAwait(false); + if (webSocket == newSocket) + webSocket = null; + } + }; + newSocket.Closed += ex => + { + Logger.Error(ex, "Connection with account verification endpoint closed unexpectedly. Please supply account verification code manually.", LoggingTarget.Network); + return Task.CompletedTask; + }; + webSocket = newSocket; + + webSocket.ConnectAsync(cancellationToken.Token).WaitSafely(); + } + catch (Exception ex) + { + Logger.Error(ex, "Failed to set up connection with account verification endpoint. Please supply account verification code manually.", LoggingTarget.Network); + } + } + public void AuthenticateSecondFactor(string code) { Debug.Assert(State.Value == APIState.RequiresSecondFactorAuth); @@ -579,6 +626,7 @@ namespace osu.Game.Online.API flushQueue(); cancellationToken.Cancel(); + webSocket?.DisposeAsync().AsTask().WaitSafely(); } } diff --git a/osu.Game/Online/DevelopmentEndpointConfiguration.cs b/osu.Game/Online/DevelopmentEndpointConfiguration.cs index 5f3c353f4d..1c78c3c147 100644 --- a/osu.Game/Online/DevelopmentEndpointConfiguration.cs +++ b/osu.Game/Online/DevelopmentEndpointConfiguration.cs @@ -13,6 +13,7 @@ namespace osu.Game.Online SpectatorEndpointUrl = $@"{APIEndpointUrl}/signalr/spectator"; MultiplayerEndpointUrl = $@"{APIEndpointUrl}/signalr/multiplayer"; MetadataEndpointUrl = $@"{APIEndpointUrl}/signalr/metadata"; + NotificationsWebSocketEndpointUrl = "wss://dev.ppy.sh/home/notifications/feed"; } } } diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs index f3bcced630..6187471b65 100644 --- a/osu.Game/Online/EndpointConfiguration.cs +++ b/osu.Game/Online/EndpointConfiguration.cs @@ -44,5 +44,10 @@ namespace osu.Game.Online /// The endpoint for the SignalR metadata server. /// public string MetadataEndpointUrl { get; set; } + + /// + /// The endpoint for the notifications websocket. + /// + public string NotificationsWebSocketEndpointUrl { get; set; } } } diff --git a/osu.Game/Online/ExperimentalEndpointConfiguration.cs b/osu.Game/Online/ExperimentalEndpointConfiguration.cs index c3d0014c8b..bc65fd63f3 100644 --- a/osu.Game/Online/ExperimentalEndpointConfiguration.cs +++ b/osu.Game/Online/ExperimentalEndpointConfiguration.cs @@ -14,6 +14,7 @@ namespace osu.Game.Online SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator"; MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer"; MetadataEndpointUrl = "https://spectator.ppy.sh/metadata"; + NotificationsWebSocketEndpointUrl = "wss://notify.ppy.sh"; } } } diff --git a/osu.Game/Online/ProductionEndpointConfiguration.cs b/osu.Game/Online/ProductionEndpointConfiguration.cs index 0244761b65..a26a25bce5 100644 --- a/osu.Game/Online/ProductionEndpointConfiguration.cs +++ b/osu.Game/Online/ProductionEndpointConfiguration.cs @@ -13,6 +13,7 @@ namespace osu.Game.Online SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator"; MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer"; MetadataEndpointUrl = "https://spectator.ppy.sh/metadata"; + NotificationsWebSocketEndpointUrl = "wss://notify.ppy.sh"; } } } From 2f8747776efa654e6d939eac867139249a75b6ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 24 Jan 2024 22:01:59 +0100 Subject: [PATCH 22/93] Fix nullability inspection --- osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs b/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs index bb78cf9fd4..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, From 21b11092d61cb6b633b2b7ab248881504aeb98b7 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 25 Jan 2024 04:06:15 +0300 Subject: [PATCH 23/93] Fix slider sliding samples allocation --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index baec200107..080d07be49 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -239,11 +239,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (Tracking.Value && Time.Current >= HitObject.StartTime) { // keep the sliding sample playing at the current tracking position - if (!slidingSample.IsPlaying) + if (!slidingSample.RequestedPlaying) slidingSample.Play(); slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball)); } - else if (slidingSample.IsPlaying) + else if (slidingSample.IsPlaying || slidingSample.RequestedPlaying) slidingSample.Stop(); } } From 8aea6e07c31127f9b93540d9b99c295f9ca3463c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jan 2024 16:31:33 +0900 Subject: [PATCH 24/93] Change slider end miss colour to gray --- osu.Game/Graphics/OsuColour.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 1b5877b966..985898958c 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -77,7 +77,7 @@ namespace osu.Game.Graphics { case HitResult.IgnoreMiss: case HitResult.SmallTickMiss: - return Orange1; + return Color4.Gray; case HitResult.Miss: case HitResult.LargeTickMiss: From dda96d71063796af8a3d4344ba080005447283e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jan 2024 16:33:52 +0900 Subject: [PATCH 25/93] Rename `JudgementPiece` to `TextJudgementPiece` --- osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs | 2 +- osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs | 2 +- osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs | 2 +- osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs | 2 +- .../Judgements/{JudgementPiece.cs => TextJudgementPiece.cs} | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) rename osu.Game/Rulesets/Judgements/{JudgementPiece.cs => TextJudgementPiece.cs} (88%) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs index a191dee1ca..0052fd8b78 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs @@ -17,7 +17,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning.Argon { - public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement + public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement { private const float judgement_y_position = 160; diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs index 9a5abba4fb..83992fc785 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs @@ -16,7 +16,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Argon { - public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement + public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement { private RingExplosion? ringExplosion; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs index bbd62ff85b..724e387cc7 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs @@ -17,7 +17,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Argon { - public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement + public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement { private RingExplosion? ringExplosion; diff --git a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs index 7330f138ce..458d79cc00 100644 --- a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs +++ b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs @@ -10,7 +10,7 @@ using osuTK; namespace osu.Game.Rulesets.Judgements { - public partial class DefaultJudgementPiece : JudgementPiece, IAnimatableJudgement + public partial class DefaultJudgementPiece : TextJudgementPiece, IAnimatableJudgement { public DefaultJudgementPiece(HitResult result) : base(result) diff --git a/osu.Game/Rulesets/Judgements/JudgementPiece.cs b/osu.Game/Rulesets/Judgements/TextJudgementPiece.cs similarity index 88% rename from osu.Game/Rulesets/Judgements/JudgementPiece.cs rename to osu.Game/Rulesets/Judgements/TextJudgementPiece.cs index 03f211c318..42527705eb 100644 --- a/osu.Game/Rulesets/Judgements/JudgementPiece.cs +++ b/osu.Game/Rulesets/Judgements/TextJudgementPiece.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Judgements { - public abstract partial class JudgementPiece : CompositeDrawable + public abstract partial class TextJudgementPiece : CompositeDrawable { protected readonly HitResult Result; @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Judgements [Resolved] private OsuColour colours { get; set; } = null!; - protected JudgementPiece(HitResult result) + protected TextJudgementPiece(HitResult result) { Result = result; } From 6070eac6eec64852a8aa27a2c283cf9388c6ad5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jan 2024 18:48:14 +0900 Subject: [PATCH 26/93] Remove dead code --- osu.Game/Skinning/LegacyJudgementPieceNew.cs | 2 +- osu.Game/Skinning/LegacyJudgementPieceOld.cs | 12 +----------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/osu.Game/Skinning/LegacyJudgementPieceNew.cs b/osu.Game/Skinning/LegacyJudgementPieceNew.cs index 5ff28726c0..bd1508b4a6 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceNew.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceNew.cs @@ -53,7 +53,7 @@ namespace osu.Game.Skinning if (!result.IsMiss()) { //new judgement shows old as a temporary effect - AddInternal(temporaryOldStyle = new LegacyJudgementPieceOld(result, createMainDrawable, 1.05f, true) + AddInternal(temporaryOldStyle = new LegacyJudgementPieceOld(result, createMainDrawable, 1.05f) { Blending = BlendingParameters.Additive, Anchor = Anchor.Centre, diff --git a/osu.Game/Skinning/LegacyJudgementPieceOld.cs b/osu.Game/Skinning/LegacyJudgementPieceOld.cs index a9f68bd378..55c81e4e41 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceOld.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceOld.cs @@ -18,16 +18,14 @@ namespace osu.Game.Skinning private readonly HitResult result; private readonly float finalScale; - private readonly bool forceTransforms; [Resolved] private ISkinSource skin { get; set; } = null!; - public LegacyJudgementPieceOld(HitResult result, Func createMainDrawable, float finalScale = 1f, bool forceTransforms = false) + public LegacyJudgementPieceOld(HitResult result, Func createMainDrawable, float finalScale = 1f) { this.result = result; this.finalScale = finalScale; - this.forceTransforms = forceTransforms; AutoSizeAxes = Axes.Both; Origin = Anchor.Centre; @@ -48,14 +46,6 @@ namespace osu.Game.Skinning this.FadeInFromZero(fade_in_length); this.Delay(fade_out_delay).FadeOut(fade_out_length); - // legacy judgements don't play any transforms if they are an animation.... UNLESS they are the temporary displayed judgement from new piece. - if (animation?.FrameCount > 1 && !forceTransforms) - { - if (isMissedTick()) - applyMissedTickScaling(); - return; - } - if (result.IsMiss()) { decimal? legacyVersion = skin.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value; From d0421fe20667530bf1bca1a5c8e3f387dde0cf6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jan 2024 18:50:45 +0900 Subject: [PATCH 27/93] Move fade more local to avoid fading twice --- osu.Game/Skinning/LegacyJudgementPieceOld.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyJudgementPieceOld.cs b/osu.Game/Skinning/LegacyJudgementPieceOld.cs index 55c81e4e41..c15ff041d1 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceOld.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceOld.cs @@ -44,7 +44,6 @@ namespace osu.Game.Skinning const double fade_out_length = 600; this.FadeInFromZero(fade_in_length); - this.Delay(fade_out_delay).FadeOut(fade_out_length); if (result.IsMiss()) { @@ -74,6 +73,8 @@ namespace osu.Game.Skinning this.RotateTo(0); this.RotateTo(rotation, fade_in_length) .Then().RotateTo(rotation * 2, fade_out_delay + fade_out_length - fade_in_length, Easing.In); + + this.Delay(fade_out_delay).FadeOut(fade_out_length); } } else @@ -87,6 +88,8 @@ namespace osu.Game.Skinning // so we need to force the current value to be correct at 1.2 (0.95) then complete the // second half of the transform. .ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f); // t = 1.4 + + this.Delay(fade_out_delay).FadeOut(fade_out_length); } } From 0175f6e0b8f5357a161211474e07af840dcf1852 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jan 2024 18:23:25 +0900 Subject: [PATCH 28/93] Add new judgement drawable for argon slider misses --- .../ArgonJudgementPieceSliderTickMiss.cs | 51 +++++++++++++++++++ .../Skinning/Argon/ArgonSliderScorePoint.cs | 4 +- .../Skinning/Argon/OsuArgonSkinTransformer.cs | 14 ++++- 3 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPieceSliderTickMiss.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPieceSliderTickMiss.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPieceSliderTickMiss.cs new file mode 100644 index 0000000000..878e8dbfc2 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPieceSliderTickMiss.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Skinning.Argon +{ + public partial class ArgonJudgementPieceSliderTickMiss : CompositeDrawable, IAnimatableJudgement + { + private readonly HitResult result; + private Circle piece = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public ArgonJudgementPieceSliderTickMiss(HitResult result) + { + this.result = result; + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(piece = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Blending = BlendingParameters.Additive, + Colour = colours.ForHitResult(result), + Size = new Vector2(ArgonSliderScorePoint.SIZE) + }); + } + + public void PlayAnimation() + { + this.ScaleTo(1.4f); + this.ScaleTo(1f, 150, Easing.Out); + + this.FadeOutFromOne(400); + } + + public Drawable? GetAboveHitObjectsProxiedContent() => piece.CreateProxy(); + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderScorePoint.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderScorePoint.cs index 7479c2aced..e9ee432bac 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderScorePoint.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderScorePoint.cs @@ -16,14 +16,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon { private Bindable accentColour = null!; - private const float size = 12; + public const float SIZE = 12; [BackgroundDependencyLoader] private void load(DrawableHitObject hitObject) { Masking = true; Origin = Anchor.Centre; - Size = new Vector2(size); + Size = new Vector2(SIZE); BorderThickness = 3; BorderColour = Color4.White; Child = new Box diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs index 0f9c97059c..ec63e1194d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs @@ -19,11 +19,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon switch (lookup) { case GameplaySkinComponentLookup resultComponent: + HitResult result = resultComponent.Component; + // This should eventually be moved to a skin setting, when supported. - if (Skin is ArgonProSkin && (resultComponent.Component == HitResult.Great || resultComponent.Component == HitResult.Perfect)) + if (Skin is ArgonProSkin && (result == HitResult.Great || result == HitResult.Perfect)) return Drawable.Empty(); - return new ArgonJudgementPiece(resultComponent.Component); + switch (result) + { + case HitResult.IgnoreMiss: + case HitResult.LargeTickMiss: + return new ArgonJudgementPieceSliderTickMiss(result); + + default: + return new ArgonJudgementPiece(result); + } case OsuSkinComponentLookup osuComponent: // TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries. From 107b37494ede1ca66a1b2cf4d669102d4c9804d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jan 2024 19:14:04 +0900 Subject: [PATCH 29/93] Update triangles skin judgment display --- .../Objects/Drawables/DrawableSliderTick.cs | 6 +-- osu.Game.Rulesets.Osu/OsuRuleset.cs | 4 ++ .../DefaultJudgementPieceSliderTickMiss.cs | 52 +++++++++++++++++++ .../Default/OsuTrianglesSkinTransformer.cs | 38 ++++++++++++++ .../Judgements/DefaultJudgementPiece.cs | 14 ----- osu.Game/Rulesets/Scoring/HitResult.cs | 2 - 6 files changed, 97 insertions(+), 19 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Skinning/Default/DefaultJudgementPieceSliderTickMiss.cs create mode 100644 osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index d64fb0bcc6..e457a50128 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public const double ANIM_DURATION = 150; - private const float default_tick_size = 16; + public const float DEFAULT_TICK_SIZE = 16; protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; @@ -44,8 +44,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { Masking = true, Origin = Anchor.Centre, - Size = new Vector2(default_tick_size), - BorderThickness = default_tick_size / 4, + Size = new Vector2(DEFAULT_TICK_SIZE), + BorderThickness = DEFAULT_TICK_SIZE / 4, BorderColour = Color4.White, Child = new Box { diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 0496d1f680..6752712be1 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -28,6 +28,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Skinning.Argon; +using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Rulesets.Osu.UI; @@ -254,6 +255,9 @@ namespace osu.Game.Rulesets.Osu case ArgonSkin: return new OsuArgonSkinTransformer(skin); + + case TrianglesSkin: + return new OsuTrianglesSkinTransformer(skin); } return null; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultJudgementPieceSliderTickMiss.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultJudgementPieceSliderTickMiss.cs new file mode 100644 index 0000000000..9fc71852ba --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultJudgementPieceSliderTickMiss.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Scoring; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Skinning.Default +{ + public partial class DefaultJudgementPieceSliderTickMiss : CompositeDrawable, IAnimatableJudgement + { + private readonly HitResult result; + private Circle piece = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public DefaultJudgementPieceSliderTickMiss(HitResult result) + { + this.result = result; + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(piece = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Blending = BlendingParameters.Additive, + Colour = colours.ForHitResult(result), + Size = new Vector2(DrawableSliderTick.DEFAULT_TICK_SIZE) + }); + } + + public void PlayAnimation() + { + this.ScaleTo(1.4f); + this.ScaleTo(1f, 150, Easing.Out); + + this.FadeOutFromOne(400); + } + + public Drawable? GetAboveHitObjectsProxiedContent() => piece.CreateProxy(); + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs new file mode 100644 index 0000000000..7a4c768aa2 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs @@ -0,0 +1,38 @@ +// 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.Graphics; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Osu.Skinning.Default +{ + public class OsuTrianglesSkinTransformer : SkinTransformer + { + public OsuTrianglesSkinTransformer(ISkin skin) + : base(skin) + { + } + + public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) + { + switch (lookup) + { + case GameplaySkinComponentLookup resultComponent: + HitResult result = resultComponent.Component; + + switch (result) + { + case HitResult.IgnoreMiss: + case HitResult.LargeTickMiss: + // use argon judgement piece for new tick misses because i don't want to design another one for triangles. + return new DefaultJudgementPieceSliderTickMiss(result); + } + + break; + } + + return base.GetDrawableComponent(lookup); + } + } +} diff --git a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs index 458d79cc00..61b72a6066 100644 --- a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs +++ b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs @@ -38,20 +38,6 @@ namespace osu.Game.Rulesets.Judgements /// public virtual void PlayAnimation() { - // TODO: make these better. currently they are using a text `-` and it's not centered properly. - // Should be an explicit drawable. - // - // When this is done, remove the [Description] attributes from HitResults which were added for this purpose. - if (Result == HitResult.IgnoreMiss || Result == HitResult.LargeTickMiss) - { - this.RotateTo(-45); - this.ScaleTo(1.6f); - this.ScaleTo(1.2f, 100, Easing.In); - - this.FadeOutFromOne(400); - return; - } - if (Result.IsMiss()) { this.ScaleTo(1.6f); diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 7e58df3cfa..20ec3c4946 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -86,7 +86,6 @@ namespace osu.Game.Rulesets.Scoring /// Indicates a large tick miss. /// [EnumMember(Value = "large_tick_miss")] - [Description("-")] [Order(11)] LargeTickMiss, @@ -118,7 +117,6 @@ namespace osu.Game.Rulesets.Scoring /// Indicates a miss that should be ignored for scoring purposes. /// [EnumMember(Value = "ignore_miss")] - [Description("-")] [Order(14)] IgnoreMiss, From fd9527d5233fd6254d4b8a627fd4b07588dbc496 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jan 2024 19:25:42 +0900 Subject: [PATCH 30/93] Remove weird red fade that didn't work --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index e457a50128..73c061afbd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -88,8 +88,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables break; case ArmedState.Miss: - this.FadeOut(ANIM_DURATION); - this.TransformBindableTo(AccentColour, Color4.Red, 0); + this.FadeOut(ANIM_DURATION, Easing.OutQuint); break; case ArmedState.Hit: From de52f0a80c088d6f6fb4bc8008a1f525d6783a9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 25 Jan 2024 12:25:27 +0100 Subject: [PATCH 31/93] Decouple notifications websocket handling from chat operations This is a prerequisite for https://github.com/ppy/osu/pull/25480. The `WebSocketNotificationsClient` was tightly coupled to chat specifics making it difficult to use in the second factor verification flow. This commit's goal is to separate the websocket connection and message handling concerns from specific chat logic concerns. --- .../Chat/TestSceneChannelManager.cs | 2 - osu.Game/Online/API/APIAccess.cs | 8 +- osu.Game/Online/API/DummyAPIAccess.cs | 8 +- osu.Game/Online/API/IAPIProvider.cs | 10 +- osu.Game/Online/Chat/ChannelManager.cs | 26 +-- osu.Game/Online/Chat/IChatClient.cs | 18 +++ osu.Game/Online/Chat/WebSocketChatClient.cs | 148 ++++++++++++++++++ .../NotificationsClientConnector.cs | 42 ----- .../WebSocket/DummyNotificationsClient.cs | 29 ++++ .../WebSocket/INotificationsClient.cs | 17 ++ .../WebSocket/WebSocketNotificationsClient.cs | 79 +--------- .../WebSocketNotificationsClientConnector.cs | 20 ++- .../PollingChatClient.cs} | 41 ++--- osu.Game/Tests/PollingChatClientConnector.cs | 48 ++++++ osu.Game/Tests/PollingNotificationsClient.cs | 35 ----- .../PollingNotificationsClientConnector.cs | 24 --- 16 files changed, 330 insertions(+), 225 deletions(-) create mode 100644 osu.Game/Online/Chat/IChatClient.cs create mode 100644 osu.Game/Online/Chat/WebSocketChatClient.cs delete mode 100644 osu.Game/Online/Notifications/NotificationsClientConnector.cs create mode 100644 osu.Game/Online/Notifications/WebSocket/DummyNotificationsClient.cs create mode 100644 osu.Game/Online/Notifications/WebSocket/INotificationsClient.cs rename osu.Game/{Online/Notifications/NotificationsClient.cs => Tests/PollingChatClient.cs} (59%) create mode 100644 osu.Game/Tests/PollingChatClientConnector.cs delete mode 100644 osu.Game/Tests/PollingNotificationsClient.cs delete mode 100644 osu.Game/Tests/PollingNotificationsClientConnector.cs 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/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 17bf8bcc37..359c52553d 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; @@ -55,6 +55,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 +84,7 @@ namespace osu.Game.Online.API APIEndpointUrl = endpointConfiguration.APIEndpointUrl; WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl; + NotificationsClient = new WebSocketNotificationsClientConnector(this); authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl); log = Logger.GetLogger(LoggingTarget.Network); @@ -324,8 +327,7 @@ namespace osu.Game.Online.API 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) { diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 4b4f8061e0..2d5852b209 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -8,7 +8,8 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; 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.Tests; using osu.Game.Users; @@ -30,6 +31,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"; @@ -144,7 +148,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 PollingChatClientConnector(this); public RegistrationRequest.RegistrationRequestErrors? CreateAccount(string email, string username, string password) { diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index b58d4a363a..ea4eb97ccb 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 @@ -129,10 +130,9 @@ namespace osu.Game.Online.API /// Whether to use MessagePack for serialisation if available on this platform. IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack = true); - /// - /// Constructs a new . - /// - NotificationsClientConnector GetNotificationsConnector(); + INotificationsClient NotificationsClient { get; } + + IChatClient GetChatClient(); /// /// Create a new user account. This is a blocking operation. diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 23989caae2..d0c686a666 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.FetchInitialMessages(); 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..94977b8acd --- /dev/null +++ b/osu.Game/Online/Chat/IChatClient.cs @@ -0,0 +1,18 @@ +// 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 +{ + public interface IChatClient : IDisposable + { + event Action? ChannelJoined; + event Action? ChannelParted; + event Action>? NewMessages; + event Action? PresenceReceived; + + void FetchInitialMessages(); + } +} diff --git a/osu.Game/Online/Chat/WebSocketChatClient.cs b/osu.Game/Online/Chat/WebSocketChatClient.cs new file mode 100644 index 0000000000..fb67c205dc --- /dev/null +++ b/osu.Game/Online/Chat/WebSocketChatClient.cs @@ -0,0 +1,148 @@ +// 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(); + } + + public void FetchInitialMessages() + { + api.Queue(createInitialFetchRequest()); + } + + private APIRequest createInitialFetchRequest() + { + 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(); + }; + + return 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..f687752047 --- /dev/null +++ b/osu.Game/Online/Notifications/WebSocket/INotificationsClient.cs @@ -0,0 +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 System; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Bindables; + +namespace osu.Game.Online.Notifications.WebSocket +{ + public interface INotificationsClient + { + IBindable IsConnected { get; } + event Action? MessageReceived; + 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/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/PollingChatClientConnector.cs b/osu.Game/Tests/PollingChatClientConnector.cs new file mode 100644 index 0000000000..3e96a8cde7 --- /dev/null +++ b/osu.Game/Tests/PollingChatClientConnector.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +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 PollingChatClientConnector : PersistentEndpointClientConnector, IChatClient + { + public event Action? ChannelJoined; + + public event Action? ChannelParted + { + add { } + remove { } + } + + public event Action>? NewMessages; + public event Action? PresenceReceived; + + public void FetchInitialMessages() + { + // don't really need to do anything special if we poll every second anyway. + } + + public PollingChatClientConnector(IAPIProvider api) + : base(api) + { + } + + 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/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)); - } -} From c463aa5ba1e67d0fc0fca5b5db5230a3875cf52a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 25 Jan 2024 14:46:39 +0100 Subject: [PATCH 32/93] xmldoc everything --- osu.Game/Online/API/IAPIProvider.cs | 6 ++++ osu.Game/Online/Chat/ChannelManager.cs | 2 +- osu.Game/Online/Chat/IChatClient.cs | 29 ++++++++++++++++--- osu.Game/Online/Chat/WebSocketChatClient.cs | 10 ++----- .../WebSocket/INotificationsClient.cs | 14 +++++++++ osu.Game/Tests/PollingChatClientConnector.cs | 2 +- 6 files changed, 50 insertions(+), 13 deletions(-) diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index ea4eb97ccb..a050b2dfae 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -130,8 +130,14 @@ namespace osu.Game.Online.API /// Whether to use MessagePack for serialisation if available on this platform. IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack = true); + /// + /// Accesses the used to receive asynchronous notifications from web. + /// INotificationsClient NotificationsClient { get; } + /// + /// Creates a instance to use in order to chat. + /// IChatClient GetChatClient(); /// diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index d0c686a666..74e85c595c 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -91,7 +91,7 @@ namespace osu.Game.Online.Chat chatClient.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false)); chatClient.NewMessages += msgs => Schedule(() => addMessages(msgs)); chatClient.PresenceReceived += () => Schedule(initializeChannels); - chatClient.FetchInitialMessages(); + chatClient.RequestPresence(); apiState.BindTo(api.State); apiState.BindValueChanged(_ => SendAck(), true); diff --git a/osu.Game/Online/Chat/IChatClient.cs b/osu.Game/Online/Chat/IChatClient.cs index 94977b8acd..290ee22710 100644 --- a/osu.Game/Online/Chat/IChatClient.cs +++ b/osu.Game/Online/Chat/IChatClient.cs @@ -6,13 +6,34 @@ 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; - event Action? ChannelParted; - event Action>? NewMessages; - event Action? PresenceReceived; - void FetchInitialMessages(); + /// + /// 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 index fb67c205dc..05d3b7b3ce 100644 --- a/osu.Game/Online/Chat/WebSocketChatClient.cs +++ b/osu.Game/Online/Chat/WebSocketChatClient.cs @@ -40,14 +40,10 @@ namespace osu.Game.Online.Chat client.MessageReceived += onMessageReceived; client.SendAsync(new StartChatRequest()).WaitSafely(); + RequestPresence(); } - public void FetchInitialMessages() - { - api.Queue(createInitialFetchRequest()); - } - - private APIRequest createInitialFetchRequest() + public void RequestPresence() { var fetchReq = new GetUpdatesRequest(0); @@ -64,7 +60,7 @@ namespace osu.Game.Online.Chat PresenceReceived?.Invoke(); }; - return fetchReq; + api.Queue(fetchReq); } private void onMessageReceived(SocketMessage message) diff --git a/osu.Game/Online/Notifications/WebSocket/INotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/INotificationsClient.cs index f687752047..9a222d0fdd 100644 --- a/osu.Game/Online/Notifications/WebSocket/INotificationsClient.cs +++ b/osu.Game/Online/Notifications/WebSocket/INotificationsClient.cs @@ -8,10 +8,24 @@ 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/Tests/PollingChatClientConnector.cs b/osu.Game/Tests/PollingChatClientConnector.cs index 3e96a8cde7..1cab24dae6 100644 --- a/osu.Game/Tests/PollingChatClientConnector.cs +++ b/osu.Game/Tests/PollingChatClientConnector.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests public event Action>? NewMessages; public event Action? PresenceReceived; - public void FetchInitialMessages() + public void RequestPresence() { // don't really need to do anything special if we poll every second anyway. } From 04cae874b0e0a6f98ba40aa048c6b6f08120c667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jan 2024 10:51:52 +0100 Subject: [PATCH 33/93] Handle forced logouts due to password change too --- osu.Game/Online/API/APIAccess.cs | 45 ++++++++++++++----------- osu.Game/Online/OnlineStatusNotifier.cs | 22 ++++++++++++ 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index ebebdbbfc2..8b369d0f3f 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -86,7 +86,7 @@ namespace osu.Game.Online.API APIEndpointUrl = endpointConfiguration.APIEndpointUrl; WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl; - NotificationsClient = new WebSocketNotificationsClientConnector(this); + NotificationsClient = setUpNotificationsClient(); authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl); log = Logger.GetLogger(LoggingTarget.Network); @@ -119,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); @@ -270,10 +294,7 @@ namespace osu.Game.Online.API setLocalUser(me); - if (me.SessionVerified) - state.Value = APIState.Online; - else - setUpSecondFactorAuthentication(); + state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth; failureCount = 0; }; @@ -356,20 +377,6 @@ namespace osu.Game.Online.API this.password = password; } - private void setUpSecondFactorAuthentication() - { - if (state.Value == APIState.RequiresSecondFactorAuth) - return; - - state.Value = APIState.RequiresSecondFactorAuth; - - NotificationsClient.MessageReceived += msg => - { - if (msg.Event == @"verified") - state.Value = APIState.Online; - }; - } - public void AuthenticateSecondFactor(string code) { Debug.Assert(State.Value == APIState.RequiresSecondFactorAuth); 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; From a2e69d37e8444a75763e9e974bf32e4eb2ce1dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jan 2024 11:17:32 +0100 Subject: [PATCH 34/93] Add basic testing of failure flow --- .../Visual/Menus/TestSceneLoginOverlay.cs | 54 +++++++++++++++++++ osu.Game/Online/API/DummyAPIAccess.cs | 22 +++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs index 0c0edca995..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,6 +10,7 @@ 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; @@ -19,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] @@ -49,13 +53,63 @@ namespace osu.Game.Tests.Visual.Menus 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] public void TestLoginFailure() { diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 08b1733aed..435c100c9a 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -7,6 +7,7 @@ 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.Chat; using osu.Game.Online.Notifications.WebSocket; @@ -133,7 +134,26 @@ namespace osu.Game.Online.API } } - public void AuthenticateSecondFactor(string code) => onSuccessfulLogin(); + 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() { From 243c2bc9b420b264e5a48b34fb4d7f3bd78d155d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jan 2024 11:21:23 +0100 Subject: [PATCH 35/93] Make sure the polling connector actually auto-starts itself --- osu.Game/Tests/PollingChatClientConnector.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Tests/PollingChatClientConnector.cs b/osu.Game/Tests/PollingChatClientConnector.cs index 1cab24dae6..f1b0d9dd7d 100644 --- a/osu.Game/Tests/PollingChatClientConnector.cs +++ b/osu.Game/Tests/PollingChatClientConnector.cs @@ -32,6 +32,7 @@ namespace osu.Game.Tests public PollingChatClientConnector(IAPIProvider api) : base(api) { + Start(); } protected sealed override Task BuildConnectionAsync(CancellationToken cancellationToken) From d38779f9620deae86c6ce0be2d0d916411c3b359 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 27 Jan 2024 23:22:29 +0300 Subject: [PATCH 36/93] Add failing test case --- .../Multiplayer/TestSceneMultiplayerPlayer.cs | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) 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); - } } } From ea641bb8d2cf7d307d508c0f1d8fb81e3ef773d7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 27 Jan 2024 19:09:46 +0300 Subject: [PATCH 37/93] Rename `GameplayState.HasFailed` to properly clarify its meaning --- .../TestSceneTaikoSuddenDeath.cs | 2 +- .../Visual/Gameplay/TestSceneFailAnimation.cs | 2 +- .../Visual/Gameplay/TestSceneFailJudgement.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestScenePause.cs | 12 ++++++------ .../Gameplay/TestScenePlayerScoreSubmission.cs | 4 ++-- .../Visual/Gameplay/TestSceneSpectator.cs | 2 +- .../Gameplay/TestSceneStoryboardWithOutro.cs | 4 ++-- .../Visual/Mods/TestSceneModAccuracyChallenge.cs | 4 ++-- .../Multiplayer/TestSceneMultiplayerPlayer.cs | 2 +- .../Navigation/TestSceneScreenNavigation.cs | 2 +- osu.Game/Online/Spectator/SpectatorClient.cs | 2 +- osu.Game/Screens/Play/GameplayState.cs | 7 +++++-- osu.Game/Screens/Play/Player.cs | 16 ++++++++-------- .../Screens/Play/PlayerTouchInputDetector.cs | 2 +- 14 files changed, 33 insertions(+), 30 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs index 9e45197b04..4b78e0a005 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests Player.ScoreProcessor.NewJudgement += _ => judged = true; }); AddUntilStep("swell judged", () => judged); - AddAssert("not failed", () => !Player.GameplayState.HasFailed); + AddAssert("not failed", () => !Player.GameplayState.ShownFailAnimation); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs index b251253b7c..369bcfac4e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { - AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); AddUntilStep("wait for fail overlay", () => ((FailPlayer)Player).FailOverlay.State.Value == Visibility.Visible); // The pause screen and fail animation both ramp frequency. diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index 6297b062dd..4fb6e9783c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { AddUntilStep("player is playing", () => Player.LocalUserPlaying.Value); - AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); AddAssert("player is not playing", () => !Player.LocalUserPlaying.Value); AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1); AddAssert("total number of results == 1", () => diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 73aa3be73d..32f5d7cc55 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -224,7 +224,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestPauseAfterFail() { - AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); AddUntilStep("fail overlay shown", () => Player.FailOverlayVisible); confirmClockRunning(false); @@ -240,7 +240,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestExitFromFailedGameplayAfterFailAnimation() { - AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); AddUntilStep("wait for fail overlay shown", () => Player.FailOverlayVisible); confirmClockRunning(false); @@ -252,7 +252,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestExitFromFailedGameplayDuringFailAnimation() { - AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); // will finish the fail animation and show the fail/pause screen. pauseViaBackAction(); @@ -266,7 +266,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestQuickRetryFromFailedGameplay() { - AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); AddStep("quick retry", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke()); confirmExited(); @@ -275,7 +275,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestQuickExitFromFailedGameplay() { - AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); exitViaQuickExitAction(); confirmExited(); @@ -380,7 +380,7 @@ namespace osu.Game.Tests.Visual.Gameplay { confirmClockRunning(false); confirmNotExited(); - AddAssert("player not failed", () => !Player.GameplayState.HasFailed); + AddAssert("player not failed", () => !Player.GameplayState.ShownFailAnimation); AddAssert("pause overlay shown", () => Player.PauseOverlayVisible); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index 5e22e47572..a4f71bcaad 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -171,7 +171,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for token request", () => Player.TokenCreationRequested); - AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); AddStep("exit", () => Player.Exit()); AddAssert("ensure no submission", () => Player.SubmittedScore == null); @@ -188,7 +188,7 @@ namespace osu.Game.Tests.Visual.Gameplay addFakeHit(); - AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); AddUntilStep("wait for submission", () => Player.SubmittedScore != null); AddAssert("ensure failing submission", () => Player.SubmittedScore.ScoreInfo.Passed == false); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 1c7ede2b19..b0bb42921d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -353,7 +353,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("send failed", () => spectatorClient.SendEndPlay(streamingUser.Id, SpectatedUserState.Failed)); AddUntilStep("state is failed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Failed); - AddUntilStep("wait for player to fail", () => player.GameplayState.HasFailed); + AddUntilStep("wait for player to fail", () => player.GameplayState.ShownFailAnimation); start(); sendFrames(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index 98825b27d4..e2e5aac734 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set storyboard duration to 0.6s", () => currentStoryboardDuration = 600); }); - AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration); AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible); } @@ -116,7 +116,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set storyboard duration to 0s", () => currentStoryboardDuration = 0); }); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration); - AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible); AddUntilStep("wait for button clickable", () => Player.ChildrenOfType().First().ChildrenOfType().First().Enabled.Value); diff --git a/osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs b/osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs index c5e56c6453..4fc6fe1b77 100644 --- a/osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs +++ b/osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Mods Position = new Vector2(i * 50) }).Cast().ToList() }, - PassCondition = () => Player.GameplayState.HasFailed && Player.ScoreProcessor.JudgedHits >= 3 + PassCondition = () => Player.GameplayState.ShownFailAnimation && Player.ScoreProcessor.JudgedHits >= 3 }); [Test] @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Mods Position = new Vector2(i * 50) }).Cast().ToList() }, - PassCondition = () => Player.GameplayState.HasFailed && Player.ScoreProcessor.JudgedHits >= 1 + PassCondition = () => Player.GameplayState.ShownFailAnimation && Player.ScoreProcessor.JudgedHits >= 1 }); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index aaf85dab7c..175fd3127d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Multiplayer 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); + AddAssert("fail animation not shown", () => !player.GameplayState.ShownFailAnimation); // 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. diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index f59fbc75ac..083a5dc833 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -392,7 +392,7 @@ namespace osu.Game.Tests.Visual.Navigation return (player = Game.ScreenStack.CurrentScreen as Player) != null; }); - AddUntilStep("wait for fail", () => player.GameplayState.HasFailed); + AddUntilStep("wait for fail", () => player.GameplayState.ShownFailAnimation); AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying); AddAssert("Ensure time before preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime); diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 7911701853..a9a666a360 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -251,7 +251,7 @@ namespace osu.Game.Online.Spectator if (state.HasPassed) currentState.State = SpectatedUserState.Passed; - else if (state.HasFailed) + else if (state.ShownFailAnimation) currentState.State = SpectatedUserState.Failed; else currentState.State = SpectatedUserState.Quit; diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index cc399a0fbe..f64bcc9a3c 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -46,9 +46,12 @@ namespace osu.Game.Screens.Play public bool HasPassed { get; set; } /// - /// Whether the user failed during gameplay. This is only set when the gameplay session has completed due to the fail. + /// Whether the user failed during gameplay and the fail animation has been displayed. /// - public bool HasFailed { get; set; } + /// + /// In multiplayer, this is never set to true even if the player reached zero health, due to being turned off. + /// + public bool ShownFailAnimation { get; set; } /// /// Whether the user quit gameplay without having either passed or failed. diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ad1f9ec897..ff1deecc3d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -489,7 +489,7 @@ namespace osu.Game.Screens.Play private void updateGameplayState() { - bool inGameplay = !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.IsPaused.Value && !breakTracker.IsBreakTime.Value && !GameplayState.HasFailed; + bool inGameplay = !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.IsPaused.Value && !breakTracker.IsBreakTime.Value && !GameplayState.ShownFailAnimation; OverlayActivationMode.Value = inGameplay ? OverlayActivation.Disabled : OverlayActivation.UserTriggered; localUserPlaying.Value = inGameplay; } @@ -586,7 +586,7 @@ namespace osu.Game.Screens.Play if (showDialogFirst && !pauseOrFailDialogVisible) { // if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion). - if (ValidForResume && GameplayState.HasFailed) + if (ValidForResume && GameplayState.ShownFailAnimation) { failAnimationContainer.FinishTransforms(true); return false; @@ -733,7 +733,7 @@ namespace osu.Game.Screens.Play } // Only show the completion screen if the player hasn't failed - if (GameplayState.HasFailed) + if (GameplayState.ShownFailAnimation) return; GameplayState.HasPassed = true; @@ -922,11 +922,11 @@ namespace osu.Game.Screens.Play if (Configuration.AllowFailAnimation) { - Debug.Assert(!GameplayState.HasFailed); + Debug.Assert(!GameplayState.ShownFailAnimation); Debug.Assert(!GameplayState.HasPassed); Debug.Assert(!GameplayState.HasQuit); - GameplayState.HasFailed = true; + GameplayState.ShownFailAnimation = true; updateGameplayState(); @@ -1002,13 +1002,13 @@ namespace osu.Game.Screens.Play // replays cannot be paused and exit immediately && !DrawableRuleset.HasReplayLoaded.Value // cannot pause if we are already in a fail state - && !GameplayState.HasFailed; + && !GameplayState.ShownFailAnimation; private bool canResume => // cannot resume from a non-paused state GameplayClockContainer.IsPaused.Value // cannot resume if we are already in a fail state - && !GameplayState.HasFailed + && !GameplayState.ShownFailAnimation // already resuming && !IsResuming; @@ -1142,7 +1142,7 @@ namespace osu.Game.Screens.Play { Debug.Assert(resultsDisplayDelegate == null); - if (!GameplayState.HasFailed) + if (!GameplayState.ShownFailAnimation) GameplayState.HasQuit = true; if (DrawableRuleset.ReplayScore == null) diff --git a/osu.Game/Screens/Play/PlayerTouchInputDetector.cs b/osu.Game/Screens/Play/PlayerTouchInputDetector.cs index 12fb748e7d..13c94f7b32 100644 --- a/osu.Game/Screens/Play/PlayerTouchInputDetector.cs +++ b/osu.Game/Screens/Play/PlayerTouchInputDetector.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Play if (!touchActive.Value) return; - if (gameplayState.HasPassed || gameplayState.HasFailed || gameplayState.HasQuit) + if (gameplayState.HasPassed || gameplayState.ShownFailAnimation || gameplayState.HasQuit) return; if (gameplayState.Score.ScoreInfo.Mods.OfType().Any()) From a25be9927df56d845f1a95f8a74bd535c9273fa0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 27 Jan 2024 19:12:03 +0300 Subject: [PATCH 38/93] Fix score processor no longer applying results when failing in multiplayer match --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 6 ------ osu.Game/Screens/Play/Player.cs | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index a092829317..7d50dd4665 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -211,9 +211,6 @@ namespace osu.Game.Rulesets.Scoring result.ComboAtJudgement = Combo.Value; result.HighestComboAtJudgement = HighestCombo.Value; - if (result.FailedAtJudgement) - return; - ScoreResultCounts[result.Type] = ScoreResultCounts.GetValueOrDefault(result.Type) + 1; if (result.Type.IncreasesCombo()) @@ -267,9 +264,6 @@ namespace osu.Game.Rulesets.Scoring Combo.Value = result.ComboAtJudgement; HighestCombo.Value = result.HighestComboAtJudgement; - if (result.FailedAtJudgement) - return; - ScoreResultCounts[result.Type] = ScoreResultCounts.GetValueOrDefault(result.Type) - 1; if (result.Judgement.MaxResult.AffectsAccuracy()) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ff1deecc3d..e2470285ba 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -357,6 +357,9 @@ namespace osu.Game.Screens.Play DrawableRuleset.NewResult += r => { + if (GameplayState.ShownFailAnimation) + return; + HealthProcessor.ApplyResult(r); ScoreProcessor.ApplyResult(r); GameplayState.ApplyResult(r); @@ -364,6 +367,9 @@ namespace osu.Game.Screens.Play DrawableRuleset.RevertResult += r => { + if (GameplayState.ShownFailAnimation) + return; + HealthProcessor.RevertResult(r); ScoreProcessor.RevertResult(r); }; From 5f689998930a5b37021a2ace9f6c76d69d07239d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 27 Jan 2024 20:44:34 +0300 Subject: [PATCH 39/93] Fix `TestSceneFailJudgement` asserts no longer being correct --- .../Visual/Gameplay/TestSceneFailJudgement.cs | 2 +- osu.Game/Rulesets/UI/Playfield.cs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index 4fb6e9783c..339746ac8b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("player is playing", () => Player.LocalUserPlaying.Value); AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); AddAssert("player is not playing", () => !Player.LocalUserPlaying.Value); - AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1); + AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).DrawableRuleset.Playfield.AllEntries.Count(e => e.Judged) > 1); AddAssert("total number of results == 1", () => { var score = new ScoreInfo { Ruleset = Ruleset.Value }; diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index e9c35555c8..176223729a 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -78,6 +78,25 @@ namespace osu.Game.Rulesets.UI } } + /// + /// All the s contained in this and all . + /// + public IEnumerable AllEntries + { + get + { + if (HitObjectContainer == null) + return Enumerable.Empty(); + + var enumerable = HitObjectContainer.Entries; + + if (nestedPlayfields.Count != 0) + enumerable = enumerable.Concat(NestedPlayfields.SelectMany(p => p.AllEntries)); + + return enumerable; + } + } + /// /// All s nested inside this . /// From 64b61108ad5b389ce136385393eb3eba9a4f9bc8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 28 Jan 2024 00:34:23 +0300 Subject: [PATCH 40/93] Move solution to multiplayer flow instead --- .../TestSceneTaikoSuddenDeath.cs | 2 +- .../Visual/Gameplay/TestSceneFailAnimation.cs | 2 +- .../Visual/Gameplay/TestSceneFailJudgement.cs | 4 ++-- .../Visual/Gameplay/TestScenePause.cs | 12 +++++----- .../TestScenePlayerScoreSubmission.cs | 4 ++-- .../Visual/Gameplay/TestSceneSpectator.cs | 2 +- .../Gameplay/TestSceneStoryboardWithOutro.cs | 4 ++-- .../Mods/TestSceneModAccuracyChallenge.cs | 4 ++-- .../Multiplayer/TestSceneMultiplayerPlayer.cs | 2 +- .../Navigation/TestSceneScreenNavigation.cs | 2 +- osu.Game/Online/Spectator/SpectatorClient.cs | 2 +- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 8 +++++++ osu.Game/Rulesets/UI/Playfield.cs | 19 ---------------- .../Multiplayer/MultiplayerPlayer.cs | 2 ++ osu.Game/Screens/Play/GameplayState.cs | 7 ++---- osu.Game/Screens/Play/Player.cs | 22 +++++++------------ .../Screens/Play/PlayerTouchInputDetector.cs | 2 +- 17 files changed, 41 insertions(+), 59 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs index 4b78e0a005..9e45197b04 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests Player.ScoreProcessor.NewJudgement += _ => judged = true; }); AddUntilStep("swell judged", () => judged); - AddAssert("not failed", () => !Player.GameplayState.ShownFailAnimation); + AddAssert("not failed", () => !Player.GameplayState.HasFailed); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs index 369bcfac4e..b251253b7c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { - AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("wait for fail overlay", () => ((FailPlayer)Player).FailOverlay.State.Value == Visibility.Visible); // The pause screen and fail animation both ramp frequency. diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index 339746ac8b..6297b062dd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -22,9 +22,9 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { AddUntilStep("player is playing", () => Player.LocalUserPlaying.Value); - AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddAssert("player is not playing", () => !Player.LocalUserPlaying.Value); - AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).DrawableRuleset.Playfield.AllEntries.Count(e => e.Judged) > 1); + AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1); AddAssert("total number of results == 1", () => { var score = new ScoreInfo { Ruleset = Ruleset.Value }; diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 32f5d7cc55..73aa3be73d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -224,7 +224,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestPauseAfterFail() { - AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("fail overlay shown", () => Player.FailOverlayVisible); confirmClockRunning(false); @@ -240,7 +240,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestExitFromFailedGameplayAfterFailAnimation() { - AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("wait for fail overlay shown", () => Player.FailOverlayVisible); confirmClockRunning(false); @@ -252,7 +252,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestExitFromFailedGameplayDuringFailAnimation() { - AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); // will finish the fail animation and show the fail/pause screen. pauseViaBackAction(); @@ -266,7 +266,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestQuickRetryFromFailedGameplay() { - AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddStep("quick retry", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke()); confirmExited(); @@ -275,7 +275,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestQuickExitFromFailedGameplay() { - AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); exitViaQuickExitAction(); confirmExited(); @@ -380,7 +380,7 @@ namespace osu.Game.Tests.Visual.Gameplay { confirmClockRunning(false); confirmNotExited(); - AddAssert("player not failed", () => !Player.GameplayState.ShownFailAnimation); + AddAssert("player not failed", () => !Player.GameplayState.HasFailed); AddAssert("pause overlay shown", () => Player.PauseOverlayVisible); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index a4f71bcaad..5e22e47572 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -171,7 +171,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for token request", () => Player.TokenCreationRequested); - AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddStep("exit", () => Player.Exit()); AddAssert("ensure no submission", () => Player.SubmittedScore == null); @@ -188,7 +188,7 @@ namespace osu.Game.Tests.Visual.Gameplay addFakeHit(); - AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("wait for submission", () => Player.SubmittedScore != null); AddAssert("ensure failing submission", () => Player.SubmittedScore.ScoreInfo.Passed == false); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index b0bb42921d..1c7ede2b19 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -353,7 +353,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("send failed", () => spectatorClient.SendEndPlay(streamingUser.Id, SpectatedUserState.Failed)); AddUntilStep("state is failed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Failed); - AddUntilStep("wait for player to fail", () => player.GameplayState.ShownFailAnimation); + AddUntilStep("wait for player to fail", () => player.GameplayState.HasFailed); start(); sendFrames(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index e2e5aac734..98825b27d4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set storyboard duration to 0.6s", () => currentStoryboardDuration = 600); }); - AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration); AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible); } @@ -116,7 +116,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set storyboard duration to 0s", () => currentStoryboardDuration = 0); }); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration); - AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible); AddUntilStep("wait for button clickable", () => Player.ChildrenOfType().First().ChildrenOfType().First().Enabled.Value); diff --git a/osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs b/osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs index 4fc6fe1b77..c5e56c6453 100644 --- a/osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs +++ b/osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Mods Position = new Vector2(i * 50) }).Cast().ToList() }, - PassCondition = () => Player.GameplayState.ShownFailAnimation && Player.ScoreProcessor.JudgedHits >= 3 + PassCondition = () => Player.GameplayState.HasFailed && Player.ScoreProcessor.JudgedHits >= 3 }); [Test] @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Mods Position = new Vector2(i * 50) }).Cast().ToList() }, - PassCondition = () => Player.GameplayState.ShownFailAnimation && Player.ScoreProcessor.JudgedHits >= 1 + PassCondition = () => Player.GameplayState.HasFailed && Player.ScoreProcessor.JudgedHits >= 1 }); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 175fd3127d..aaf85dab7c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Multiplayer 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.ShownFailAnimation); + 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. diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 083a5dc833..f59fbc75ac 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -392,7 +392,7 @@ namespace osu.Game.Tests.Visual.Navigation return (player = Game.ScreenStack.CurrentScreen as Player) != null; }); - AddUntilStep("wait for fail", () => player.GameplayState.ShownFailAnimation); + AddUntilStep("wait for fail", () => player.GameplayState.HasFailed); AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying); AddAssert("Ensure time before preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime); diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index a9a666a360..7911701853 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -251,7 +251,7 @@ namespace osu.Game.Online.Spectator if (state.HasPassed) currentState.State = SpectatedUserState.Passed; - else if (state.ShownFailAnimation) + else if (state.HasFailed) currentState.State = SpectatedUserState.Failed; else currentState.State = SpectatedUserState.Quit; diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 7d50dd4665..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,6 +213,9 @@ namespace osu.Game.Rulesets.Scoring result.ComboAtJudgement = Combo.Value; result.HighestComboAtJudgement = HighestCombo.Value; + if (result.FailedAtJudgement && !ApplyNewJudgementsWhenFailed) + return; + ScoreResultCounts[result.Type] = ScoreResultCounts.GetValueOrDefault(result.Type) + 1; if (result.Type.IncreasesCombo()) @@ -264,6 +269,9 @@ namespace osu.Game.Rulesets.Scoring Combo.Value = result.ComboAtJudgement; HighestCombo.Value = result.HighestComboAtJudgement; + if (result.FailedAtJudgement && !ApplyNewJudgementsWhenFailed) + return; + ScoreResultCounts[result.Type] = ScoreResultCounts.GetValueOrDefault(result.Type) - 1; if (result.Judgement.MaxResult.AffectsAccuracy()) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 176223729a..e9c35555c8 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -78,25 +78,6 @@ namespace osu.Game.Rulesets.UI } } - /// - /// All the s contained in this and all . - /// - public IEnumerable AllEntries - { - get - { - if (HitObjectContainer == null) - return Enumerable.Empty(); - - var enumerable = HitObjectContainer.Entries; - - if (nestedPlayfields.Count != 0) - enumerable = enumerable.Concat(NestedPlayfields.SelectMany(p => p.AllEntries)); - - return enumerable; - } - } - /// /// All s nested inside this . /// 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/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index f64bcc9a3c..cc399a0fbe 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -46,12 +46,9 @@ namespace osu.Game.Screens.Play public bool HasPassed { get; set; } /// - /// Whether the user failed during gameplay and the fail animation has been displayed. + /// Whether the user failed during gameplay. This is only set when the gameplay session has completed due to the fail. /// - /// - /// In multiplayer, this is never set to true even if the player reached zero health, due to being turned off. - /// - public bool ShownFailAnimation { get; set; } + public bool HasFailed { get; set; } /// /// Whether the user quit gameplay without having either passed or failed. diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e2470285ba..ad1f9ec897 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -357,9 +357,6 @@ namespace osu.Game.Screens.Play DrawableRuleset.NewResult += r => { - if (GameplayState.ShownFailAnimation) - return; - HealthProcessor.ApplyResult(r); ScoreProcessor.ApplyResult(r); GameplayState.ApplyResult(r); @@ -367,9 +364,6 @@ namespace osu.Game.Screens.Play DrawableRuleset.RevertResult += r => { - if (GameplayState.ShownFailAnimation) - return; - HealthProcessor.RevertResult(r); ScoreProcessor.RevertResult(r); }; @@ -495,7 +489,7 @@ namespace osu.Game.Screens.Play private void updateGameplayState() { - bool inGameplay = !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.IsPaused.Value && !breakTracker.IsBreakTime.Value && !GameplayState.ShownFailAnimation; + bool inGameplay = !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.IsPaused.Value && !breakTracker.IsBreakTime.Value && !GameplayState.HasFailed; OverlayActivationMode.Value = inGameplay ? OverlayActivation.Disabled : OverlayActivation.UserTriggered; localUserPlaying.Value = inGameplay; } @@ -592,7 +586,7 @@ namespace osu.Game.Screens.Play if (showDialogFirst && !pauseOrFailDialogVisible) { // if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion). - if (ValidForResume && GameplayState.ShownFailAnimation) + if (ValidForResume && GameplayState.HasFailed) { failAnimationContainer.FinishTransforms(true); return false; @@ -739,7 +733,7 @@ namespace osu.Game.Screens.Play } // Only show the completion screen if the player hasn't failed - if (GameplayState.ShownFailAnimation) + if (GameplayState.HasFailed) return; GameplayState.HasPassed = true; @@ -928,11 +922,11 @@ namespace osu.Game.Screens.Play if (Configuration.AllowFailAnimation) { - Debug.Assert(!GameplayState.ShownFailAnimation); + Debug.Assert(!GameplayState.HasFailed); Debug.Assert(!GameplayState.HasPassed); Debug.Assert(!GameplayState.HasQuit); - GameplayState.ShownFailAnimation = true; + GameplayState.HasFailed = true; updateGameplayState(); @@ -1008,13 +1002,13 @@ namespace osu.Game.Screens.Play // replays cannot be paused and exit immediately && !DrawableRuleset.HasReplayLoaded.Value // cannot pause if we are already in a fail state - && !GameplayState.ShownFailAnimation; + && !GameplayState.HasFailed; private bool canResume => // cannot resume from a non-paused state GameplayClockContainer.IsPaused.Value // cannot resume if we are already in a fail state - && !GameplayState.ShownFailAnimation + && !GameplayState.HasFailed // already resuming && !IsResuming; @@ -1148,7 +1142,7 @@ namespace osu.Game.Screens.Play { Debug.Assert(resultsDisplayDelegate == null); - if (!GameplayState.ShownFailAnimation) + if (!GameplayState.HasFailed) GameplayState.HasQuit = true; if (DrawableRuleset.ReplayScore == null) diff --git a/osu.Game/Screens/Play/PlayerTouchInputDetector.cs b/osu.Game/Screens/Play/PlayerTouchInputDetector.cs index 13c94f7b32..12fb748e7d 100644 --- a/osu.Game/Screens/Play/PlayerTouchInputDetector.cs +++ b/osu.Game/Screens/Play/PlayerTouchInputDetector.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Play if (!touchActive.Value) return; - if (gameplayState.HasPassed || gameplayState.ShownFailAnimation || gameplayState.HasQuit) + if (gameplayState.HasPassed || gameplayState.HasFailed || gameplayState.HasQuit) return; if (gameplayState.Score.ScoreInfo.Mods.OfType().Any()) From 9c55498058abbb9b5563319abbc942a186b556cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jan 2024 15:28:14 +0900 Subject: [PATCH 41/93] r# says hi --- .../NonVisual/Skinning/LegacySkinTextureFallbackTest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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; } } From de32e7815b19d8ea5a19114eebc67d3f85ecaa27 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jan 2024 14:57:19 +0900 Subject: [PATCH 42/93] Clean up `DrawableHitObject` events on `Dispose` This is just general safeties to avoid cases where components don't correctly unbind events. --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 7 +++++++ osu.Game/Rulesets/Objects/HitObject.cs | 2 ++ 2 files changed, 9 insertions(+) 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(); From 76832a1495d2d94fc433e46d7413af69cd9e3463 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jan 2024 15:15:10 +0900 Subject: [PATCH 43/93] Remove `ScorePerformanceCache` This class was only used in two places, both on the results screen, but was holding references to `OsuPlayfield` game-wide (due to unrelated issues, but still). Because I can't really think of future use cases for this, and running the calculation twice at results screen isn't a huge overhead, let's just do that for now to keep things simple. --- osu.Game/OsuGameBase.cs | 4 -- .../PerformanceBreakdownCalculator.cs | 24 +++++-- .../Difficulty/PerformanceCalculator.cs | 5 ++ osu.Game/Scoring/ScorePerformanceCache.cs | 68 ------------------- .../Statistics/PerformanceStatistic.cs | 20 ++++-- .../Statistics/PerformanceBreakdownChart.cs | 5 +- 6 files changed, 40 insertions(+), 86 deletions(-) delete mode 100644 osu.Game/Scoring/ScorePerformanceCache.cs 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/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/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/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/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()))); } From eb90ee5415b05dd827464fcd3c715f731da2df1f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jan 2024 15:39:31 +0900 Subject: [PATCH 44/93] Add safety in `ResultsScreen.Exit` to ensure `HitEvents` are not holding references This is a catch-all safety disconnecting `ScoreInfo` from `HitObject`s. --- osu.Game/Screens/Ranking/ResultsScreen.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 697d62ad6e..410fdf7090 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -275,6 +275,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; } From 760368709a4855d0c64a89b03d455a8dd514b987 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jan 2024 16:12:00 +0900 Subject: [PATCH 45/93] Mark some delegates as static because we can --- osu.Game.Rulesets.Osu/OsuInputManager.cs | 2 +- osu.Game/Rulesets/UI/RulesetInputManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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); } } From fb24c663425648481a0e6f2006801cc0165e34dc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jan 2024 16:13:30 +0900 Subject: [PATCH 46/93] Mark `ResultsScreen.Score` as nullable Is nullable in playlist results at very least. --- .../Visual/Gameplay/TestScenePlayerLocalScoreImport.cs | 4 ++-- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 2 +- .../Visual/Playlists/TestScenePlaylistsResultsScreen.cs | 2 +- .../Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs | 2 +- osu.Game/Screens/Play/SpectatorResultsScreen.cs | 2 +- osu.Game/Screens/Ranking/ResultsScreen.cs | 6 ++++-- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 7 +++++++ 7 files changed, 17 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs index fafd1330cc..c645b1b01c 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())); 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/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/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/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/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 410fdf7090..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; @@ -278,7 +280,7 @@ namespace osu.Game.Screens.Ranking // 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(); + 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; From a41ba7c381ba66ca432a34dd4ecc9cd655119772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Jan 2024 08:43:16 +0100 Subject: [PATCH 47/93] Fix more nullable inspections --- osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs | 4 ++-- osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs | 2 +- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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/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/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; } From ef94eff5745e577cec54615d6c69522100b20b87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jan 2024 16:56:28 +0900 Subject: [PATCH 48/93] Rename `PollingChatClientConnector` to better describe usage --- osu.Game/Online/API/DummyAPIAccess.cs | 2 +- ...llingChatClientConnector.cs => TestChatClientConnector.cs} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename osu.Game/Tests/{PollingChatClientConnector.cs => TestChatClientConnector.cs} (89%) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 2d5852b209..3c60513495 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -148,7 +148,7 @@ namespace osu.Game.Online.API public IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null; - public IChatClient GetChatClient() => new PollingChatClientConnector(this); + public IChatClient GetChatClient() => new TestChatClientConnector(this); public RegistrationRequest.RegistrationRequestErrors? CreateAccount(string email, string username, string password) { diff --git a/osu.Game/Tests/PollingChatClientConnector.cs b/osu.Game/Tests/TestChatClientConnector.cs similarity index 89% rename from osu.Game/Tests/PollingChatClientConnector.cs rename to osu.Game/Tests/TestChatClientConnector.cs index f1b0d9dd7d..40e15b5ef5 100644 --- a/osu.Game/Tests/PollingChatClientConnector.cs +++ b/osu.Game/Tests/TestChatClientConnector.cs @@ -11,7 +11,7 @@ using osu.Game.Online.Chat; namespace osu.Game.Tests { - public class PollingChatClientConnector : PersistentEndpointClientConnector, IChatClient + public class TestChatClientConnector : PersistentEndpointClientConnector, IChatClient { public event Action? ChannelJoined; @@ -29,7 +29,7 @@ namespace osu.Game.Tests // don't really need to do anything special if we poll every second anyway. } - public PollingChatClientConnector(IAPIProvider api) + public TestChatClientConnector(IAPIProvider api) : base(api) { Start(); From 363fd1d54f76c82e639d9394aaa05e1a0cbfa28b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Jan 2024 09:05:03 +0100 Subject: [PATCH 49/93] Remove no longer relevant changes --- .../DevelopmentEndpointConfiguration.cs | 1 - osu.Game/Online/EndpointConfiguration.cs | 5 - .../ExperimentalEndpointConfiguration.cs | 1 - .../WebSocket/OsuClientWebSocket.cs | 154 ------------------ .../Online/ProductionEndpointConfiguration.cs | 1 - 5 files changed, 162 deletions(-) delete mode 100644 osu.Game/Online/Notifications/WebSocket/OsuClientWebSocket.cs diff --git a/osu.Game/Online/DevelopmentEndpointConfiguration.cs b/osu.Game/Online/DevelopmentEndpointConfiguration.cs index 1c78c3c147..5f3c353f4d 100644 --- a/osu.Game/Online/DevelopmentEndpointConfiguration.cs +++ b/osu.Game/Online/DevelopmentEndpointConfiguration.cs @@ -13,7 +13,6 @@ namespace osu.Game.Online SpectatorEndpointUrl = $@"{APIEndpointUrl}/signalr/spectator"; MultiplayerEndpointUrl = $@"{APIEndpointUrl}/signalr/multiplayer"; MetadataEndpointUrl = $@"{APIEndpointUrl}/signalr/metadata"; - NotificationsWebSocketEndpointUrl = "wss://dev.ppy.sh/home/notifications/feed"; } } } diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs index 6187471b65..f3bcced630 100644 --- a/osu.Game/Online/EndpointConfiguration.cs +++ b/osu.Game/Online/EndpointConfiguration.cs @@ -44,10 +44,5 @@ namespace osu.Game.Online /// The endpoint for the SignalR metadata server. /// public string MetadataEndpointUrl { get; set; } - - /// - /// The endpoint for the notifications websocket. - /// - public string NotificationsWebSocketEndpointUrl { get; set; } } } diff --git a/osu.Game/Online/ExperimentalEndpointConfiguration.cs b/osu.Game/Online/ExperimentalEndpointConfiguration.cs index bc65fd63f3..c3d0014c8b 100644 --- a/osu.Game/Online/ExperimentalEndpointConfiguration.cs +++ b/osu.Game/Online/ExperimentalEndpointConfiguration.cs @@ -14,7 +14,6 @@ namespace osu.Game.Online SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator"; MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer"; MetadataEndpointUrl = "https://spectator.ppy.sh/metadata"; - NotificationsWebSocketEndpointUrl = "wss://notify.ppy.sh"; } } } diff --git a/osu.Game/Online/Notifications/WebSocket/OsuClientWebSocket.cs b/osu.Game/Online/Notifications/WebSocket/OsuClientWebSocket.cs deleted file mode 100644 index 965f606bdc..0000000000 --- a/osu.Game/Online/Notifications/WebSocket/OsuClientWebSocket.cs +++ /dev/null @@ -1,154 +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.Diagnostics; -using System.Net; -using System.Net.WebSockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; -using osu.Framework.Extensions.TypeExtensions; -using osu.Framework.Logging; -using osu.Game.Online.API; - -namespace osu.Game.Online.Notifications.WebSocket -{ - public class OsuClientWebSocket : IAsyncDisposable - { - public event Func? MessageReceived; - public event Func? Closed; - - private readonly string endpoint; - private readonly ClientWebSocket socket; - - private CancellationTokenSource? linkedTokenSource = null; - - public OsuClientWebSocket(IAPIProvider api, string endpoint) - { - socket = new ClientWebSocket(); - socket.Options.SetRequestHeader(@"Authorization", @$"Bearer {api.AccessToken}"); - socket.Options.Proxy = WebRequest.DefaultWebProxy; - if (socket.Options.Proxy != null) - socket.Options.Proxy.Credentials = CredentialCache.DefaultCredentials; - - this.endpoint = endpoint; - } - - public async Task ConnectAsync(CancellationToken cancellationToken) - { - if (socket.State == WebSocketState.Connecting || socket.State == WebSocketState.Open) - throw new InvalidOperationException("Connection is already opened"); - - Debug.Assert(linkedTokenSource == null); - linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - - await socket.ConnectAsync(new Uri(endpoint), linkedTokenSource.Token).ConfigureAwait(false); - runReadLoop(linkedTokenSource.Token); - } - - private void runReadLoop(CancellationToken cancellationToken) => Task.Factory.StartNew(async () => - { - byte[] buffer = new byte[1024]; - StringBuilder messageResult = new StringBuilder(); - - while (!cancellationToken.IsCancellationRequested) - { - try - { - WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false); - - switch (result.MessageType) - { - case WebSocketMessageType.Text: - messageResult.Append(Encoding.UTF8.GetString(buffer[..result.Count])); - - if (result.EndOfMessage) - { - SocketMessage? message = JsonConvert.DeserializeObject(messageResult.ToString()); - messageResult.Clear(); - - Debug.Assert(message != null); - - if (message.Error != null) - { - Logger.Log($"{GetType().ReadableName()} error: {message.Error}", LoggingTarget.Network); - break; - } - - await invokeMessageReceived(message).ConfigureAwait(false); - } - - break; - - case WebSocketMessageType.Binary: - throw new NotImplementedException("Binary message type not supported."); - - case WebSocketMessageType.Close: - throw new WebException("Connection closed by remote host."); - } - } - catch (Exception ex) - { - await invokeClosed(ex).ConfigureAwait(false); - return; - } - } - }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default); - - private async Task invokeMessageReceived(SocketMessage message) - { - if (MessageReceived == null) - return; - - var invocationList = MessageReceived.GetInvocationList(); - - // ReSharper disable once PossibleInvalidCastExceptionInForeachLoop - foreach (Func handler in invocationList) - await handler.Invoke(message).ConfigureAwait(false); - } - - private async Task invokeClosed(Exception ex) - { - if (Closed == null) - return; - - var invocationList = Closed.GetInvocationList(); - - // ReSharper disable once PossibleInvalidCastExceptionInForeachLoop - foreach (Func handler in invocationList) - await handler.Invoke(ex).ConfigureAwait(false); - } - - public Task SendMessage(SocketMessage message, CancellationToken cancellationToken) - { - if (socket.State != WebSocketState.Open) - return Task.CompletedTask; - - return socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken); - } - - public async Task DisconnectAsync() - { - linkedTokenSource?.Cancel(); - await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, @"Disconnecting", CancellationToken.None).ConfigureAwait(false); - linkedTokenSource?.Dispose(); - linkedTokenSource = null; - } - - public async ValueTask DisposeAsync() - { - try - { - await DisconnectAsync().ConfigureAwait(false); - } - catch - { - // Closure can fail if the connection is aborted. Don't really care since it's disposed anyway. - } - - socket.Dispose(); - } - } -} diff --git a/osu.Game/Online/ProductionEndpointConfiguration.cs b/osu.Game/Online/ProductionEndpointConfiguration.cs index a26a25bce5..0244761b65 100644 --- a/osu.Game/Online/ProductionEndpointConfiguration.cs +++ b/osu.Game/Online/ProductionEndpointConfiguration.cs @@ -13,7 +13,6 @@ namespace osu.Game.Online SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator"; MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer"; MetadataEndpointUrl = "https://spectator.ppy.sh/metadata"; - NotificationsWebSocketEndpointUrl = "wss://notify.ppy.sh"; } } } From 96811a88749b7cc763f721ecebca20006ed08289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Jan 2024 09:13:44 +0100 Subject: [PATCH 50/93] Fix `APIAccess` spamming requests while waiting for second factor --- osu.Game/Online/API/APIAccess.cs | 60 +++++++++++++++++--------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 8b369d0f3f..52506e4b12 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -269,42 +269,44 @@ namespace osu.Game.Online.API } } - var userReq = new GetMeRequest(); - userReq.Failure += ex => + if (state.Value != APIState.RequiresSecondFactorAuth) { - if (ex is APIException) + var userReq = new GetMeRequest(); + userReq.Failure += ex => { - LastLoginError = ex; - log.Add($@"Login failed for username {ProvidedUsername} on user retrieval ({LastLoginError.Message})!"); - Logout(); - } - else if (ex is WebException webException && webException.Message == @"Unauthorized") + 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 => { - log.Add(@"Login no longer valid"); - Logout(); - } - else + 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; } - }; - 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; } - - if (state.Value == APIState.RequiresSecondFactorAuth) + else { if (string.IsNullOrEmpty(SecondFactorCode)) return; From 6a469f2cb6e45ec46c435682dc99e6e0d09b3a82 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jan 2024 17:18:17 +0900 Subject: [PATCH 51/93] Use `switch` instead of `if-else` --- osu.Game/Online/API/APIAccess.cs | 114 +++++++++++++++++-------------- 1 file changed, 62 insertions(+), 52 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 52506e4b12..d3707fe74d 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -269,69 +269,79 @@ namespace osu.Game.Online.API } } - if (state.Value != APIState.RequiresSecondFactorAuth) + switch (state.Value) { - var userReq = new GetMeRequest(); - userReq.Failure += ex => + case APIState.RequiresSecondFactorAuth: { - if (ex is APIException) + 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; - 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 + SecondFactorCode = null; + }; + + if (!handleRequest(verificationRequest)) { state.Value = APIState.Failing; + return; } - }; - userReq.Success += me => - { - me.Status.Value = configStatus.Value ?? UserStatus.Online; - setLocalUser(me); + if (state.Value != APIState.Online) + return; - state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth; - failureCount = 0; - }; - - if (!handleRequest(userReq)) - { - state.Value = APIState.Failing; - return; - } - } - else - { - 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; + break; } - if (state.Value != APIState.Online) - return; + default: + { + 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; + } } var friendsReq = new GetFriendsRequest(); From 540ff0da5b1192a9f442952973ddb4ae02bdb80c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jan 2024 17:54:34 +0900 Subject: [PATCH 52/93] Add loading layer when requesting a code reissue --- .../Overlays/Login/SecondFactorAuthForm.cs | 82 ++++++++++++------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs index 566587a541..dcd3119f33 100644 --- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.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.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -17,12 +18,14 @@ using osuTK; namespace osu.Game.Overlays.Login { - public partial class SecondFactorAuthForm : FillFlowContainer + 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!; @@ -31,8 +34,6 @@ namespace osu.Game.Overlays.Login { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Direction = FillDirection.Vertical; - Spacing = new Vector2(0, SettingsSection.ITEM_SPACING); Children = new Drawable[] { @@ -40,42 +41,56 @@ namespace osu.Game.Overlays.Login { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }, Direction = FillDirection.Vertical, - Spacing = new Vector2(0f, SettingsSection.ITEM_SPACING), + Spacing = new Vector2(0, SettingsSection.ITEM_SPACING), Children = new Drawable[] { - new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular)) + new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Text = "An email has been sent to you with a verification code. Enter the code.", + 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, + }, + }, }, - codeTextBox = new OsuTextBox - { - PlaceholderText = "Enter code", - RelativeSizeAxes = Axes.X, - TabbableContentContainer = this, - }, - explainText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular)) + new LinkFlowContainer { + Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, }, - errorText = new ErrorTextFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - }, - }, + } }, - new LinkFlowContainer + loading = new LoadingLayer(true) { - Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }, + Padding = new MarginPadding { Vertical = -SettingsSection.ITEM_SPACING }, + } }; explainText.AddParagraph(UserVerificationStrings.BoxInfoCheckSpam); @@ -85,9 +100,20 @@ namespace osu.Game.Overlays.Login 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."); - api.Perform(reissueRequest); + 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(); }); From dc28a6b873f0af9835f991acb805e647123e8446 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jan 2024 19:33:07 +0900 Subject: [PATCH 53/93] Update resources --- osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs | 2 +- osu.Game/osu.Game.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index e46b92795a..a552b22c11 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Backgrounds private Background background; private int currentDisplay; - private const int background_count = 7; + private const int background_count = 8; private IBindable user; private Bindable skin; private Bindable source; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 6f71424130..48ba7beb7d 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From 8341da7586126de557904f4db124113d33ef9e4d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jan 2024 19:41:03 +0900 Subject: [PATCH 54/93] Revert "Remove dead code" (mostly) This reverts commit 6070eac6eec64852a8aa27a2c283cf9388c6ad5c. --- osu.Game/Skinning/LegacyJudgementPieceNew.cs | 2 +- osu.Game/Skinning/LegacyJudgementPieceOld.cs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Skinning/LegacyJudgementPieceNew.cs b/osu.Game/Skinning/LegacyJudgementPieceNew.cs index bd1508b4a6..5ff28726c0 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceNew.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceNew.cs @@ -53,7 +53,7 @@ namespace osu.Game.Skinning if (!result.IsMiss()) { //new judgement shows old as a temporary effect - AddInternal(temporaryOldStyle = new LegacyJudgementPieceOld(result, createMainDrawable, 1.05f) + AddInternal(temporaryOldStyle = new LegacyJudgementPieceOld(result, createMainDrawable, 1.05f, true) { Blending = BlendingParameters.Additive, Anchor = Anchor.Centre, diff --git a/osu.Game/Skinning/LegacyJudgementPieceOld.cs b/osu.Game/Skinning/LegacyJudgementPieceOld.cs index c15ff041d1..edfb5a23ec 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceOld.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceOld.cs @@ -18,14 +18,16 @@ namespace osu.Game.Skinning private readonly HitResult result; private readonly float finalScale; + private readonly bool forceTransforms; [Resolved] private ISkinSource skin { get; set; } = null!; - public LegacyJudgementPieceOld(HitResult result, Func createMainDrawable, float finalScale = 1f) + public LegacyJudgementPieceOld(HitResult result, Func createMainDrawable, float finalScale = 1f, bool forceTransforms = false) { this.result = result; this.finalScale = finalScale; + this.forceTransforms = forceTransforms; AutoSizeAxes = Axes.Both; Origin = Anchor.Centre; @@ -45,6 +47,10 @@ namespace osu.Game.Skinning this.FadeInFromZero(fade_in_length); + // legacy judgements don't play any transforms if they are an animation.... UNLESS they are the temporary displayed judgement from new piece. + if (animation?.FrameCount > 1 && !forceTransforms) + return; + if (result.IsMiss()) { decimal? legacyVersion = skin.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value; @@ -95,12 +101,6 @@ namespace osu.Game.Skinning private bool isMissedTick() => result.IsMiss() && result != HitResult.Miss; - private void applyMissedTickScaling() - { - this.ScaleTo(0.6f); - this.ScaleTo(0.3f, 100, Easing.In); - } - public Drawable GetAboveHitObjectsProxiedContent() => CreateProxy(); } } From 1efdf2ae253a9844dc437c6f26767efb17bfb06a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jan 2024 19:45:56 +0900 Subject: [PATCH 55/93] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 969fd52340..da404599ef 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index bbcabc6360..0855c0c6d6 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 8a11ff122712a15a2f466f01434af76e00ac98f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Jan 2024 11:46:45 +0100 Subject: [PATCH 56/93] Apply local precision workaround to editor effect section --- osu.Game/Screens/Edit/Timing/EffectSection.cs | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs index 7e484433f7..f321f7eeb0 100644 --- a/osu.Game/Screens/Edit/Timing/EffectSection.cs +++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs @@ -52,17 +52,38 @@ namespace osu.Game.Screens.Edit.Timing protected override void OnControlPointChanged(ValueChangedEvent point) { - if (point.NewValue != null) + scrollSpeedSlider.Current.ValueChanged -= updateControlPointFromSlider; + + if (point.NewValue is EffectControlPoint newEffectPoint) { isRebinding = true; - kiai.Current = point.NewValue.KiaiModeBindable; - scrollSpeedSlider.Current = point.NewValue.ScrollSpeedBindable; + kiai.Current = newEffectPoint.KiaiModeBindable; + scrollSpeedSlider.Current = new BindableDouble + { + MinValue = 0.01, + MaxValue = 10, + Precision = 0.01, + Value = newEffectPoint.ScrollSpeedBindable.Value + }; + scrollSpeedSlider.Current.ValueChanged += updateControlPointFromSlider; + // at this point in time the above is enough to keep the slider control in sync with reality, + // since undo/redo causes `OnControlPointChanged()` to fire. + // whenever that stops being the case, or there is a possibility that the scroll speed could be changed + // by something else other than this control, this code should probably be revisited to have a binding in the other direction, too. isRebinding = false; } } + private void updateControlPointFromSlider(ValueChangedEvent scrollSpeed) + { + if (ControlPoint.Value is not EffectControlPoint effectPoint || isRebinding) + return; + + effectPoint.ScrollSpeedBindable.Value = scrollSpeed.NewValue; + } + protected override EffectControlPoint CreatePoint() { var reference = Beatmap.ControlPointInfo.EffectPointAt(SelectedGroup.Value.Time); From 034f8c0388c84333961d5ad6702c044dc83e519f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jan 2024 20:36:59 +0900 Subject: [PATCH 57/93] Also fix spinner case --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index bf4b07eaab..9a98286738 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { if (tracking.NewValue) { - if (!spinningSample.IsPlaying) + if (!spinningSample.RequestedPlaying) spinningSample.Play(); spinningSample.VolumeTo(1, 300); From f2546d72c2616f4d3d746d27b9fea061998591d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jan 2024 20:45:51 +0900 Subject: [PATCH 58/93] Fix osu! logo being mispositioned in song select on very wide resolutions --- osu.Game/Screens/Select/SongSelect.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index bf1724995a..50467bc089 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -642,7 +642,11 @@ namespace osu.Game.Screens.Select { base.LogoArriving(logo, resuming); - Vector2 position = new Vector2(0.95f, 0.96f); + logo.Anchor = Anchor.BottomRight; + logo.Origin = Anchor.Centre; + logo.RelativePositionAxes = Axes.None; + + Vector2 position = new Vector2(-76, -36); if (logo.Alpha > 0.8f) { From 16c06169ed077bc493e1856094b76528de1665d6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Jan 2024 01:06:32 +0900 Subject: [PATCH 59/93] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index da404599ef..55ef55ab7d 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 0855c0c6d6..5b99319499 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 996ae0ecc1f970ebc60f17653b3cf6b698794dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Jan 2024 19:17:39 +0100 Subject: [PATCH 60/93] Fix incorrect handling of `StartChatRequest` in websocket chat client Fixes this happening on staging: [network] 2024-01-29 17:48:24 [verbose]: WebSocketNotificationsClientConnector connected! [network] 2024-01-29 17:48:24 [verbose]: WebSocketNotificationsClientConnector connect attempt failed: Can't use WaitSafely from inside an async operation. I'm not sure how I ever allowed that `.WaitSafely()` to be there. It did feel rather dangerous but then I must have forgotten and never noticed it failing. Which is weird because you'd think that would be caught by testing that chat isn't working but I'm pretty sure that I tested that chat *was* indeed working. Anyway now that entire flow is replaced by something that should hopefully be somewhat more sane? It has a whole retry flow with logging now which should be more robust than what was there previously (failing to start to listen to chat events killing the entire websocket connection for very little good reason). --- osu.Game/Online/Chat/WebSocketChatClient.cs | 47 +++++++++++++++++---- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/osu.Game/Online/Chat/WebSocketChatClient.cs b/osu.Game/Online/Chat/WebSocketChatClient.cs index 05d3b7b3ce..b74f8bec4b 100644 --- a/osu.Game/Online/Chat/WebSocketChatClient.cs +++ b/osu.Game/Online/Chat/WebSocketChatClient.cs @@ -5,9 +5,10 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; 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; @@ -26,21 +27,49 @@ namespace osu.Game.Online.Chat private readonly INotificationsClient client; private readonly ConcurrentDictionary channelsMap = new ConcurrentDictionary(); + private CancellationTokenSource? chatStartCancellationSource; + public WebSocketChatClient(IAPIProvider api) { this.api = api; client = api.NotificationsClient; - client.IsConnected.BindValueChanged(start, true); + client.IsConnected.BindValueChanged(onConnectedChanged, true); } - private void start(ValueChangedEvent connected) + private void onConnectedChanged(ValueChangedEvent connected) { - if (!connected.NewValue) - return; + if (connected.NewValue) + { + client.MessageReceived += onMessageReceived; + attemptToStartChat(); + RequestPresence(); + } + else + chatStartCancellationSource?.Cancel(); + } - client.MessageReceived += onMessageReceived; - client.SendAsync(new StartChatRequest()).WaitSafely(); - RequestPresence(); + private void attemptToStartChat() + { + chatStartCancellationSource?.Cancel(); + chatStartCancellationSource = new CancellationTokenSource(); + + Task.Factory.StartNew(async () => + { + while (!chatStartCancellationSource.IsCancellationRequested) + { + try + { + await client.SendAsync(new StartChatRequest()).ConfigureAwait(false); + Logger.Log(@"Now listening to websocket chat messages.", LoggingTarget.Network); + chatStartCancellationSource.Cancel(); + } + catch (Exception ex) + { + Logger.Log($@"Could not start listening to websocket chat messages: {ex}", LoggingTarget.Network); + await Task.Delay(5000).ConfigureAwait(false); + } + } + }, chatStartCancellationSource.Token); } public void RequestPresence() @@ -137,7 +166,7 @@ namespace osu.Game.Online.Chat public void Dispose() { - client.IsConnected.ValueChanged -= start; + client.IsConnected.ValueChanged -= onConnectedChanged; client.MessageReceived -= onMessageReceived; } } From 959cc7c7d91fa288bb98777b73286bc30285a21f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 29 Jan 2024 21:26:36 +0300 Subject: [PATCH 61/93] Rewrite time range computation logic to be completely based on stable code --- .../UI/DrawableTaikoRuleset.cs | 20 ++-------- .../UI/TaikoPlayfieldAdjustmentContainer.cs | 40 +++++++++++++++++-- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index 49b0ad811d..77b2b06c0e 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -16,7 +16,6 @@ using osu.Game.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Replays; using osu.Game.Rulesets.Timing; @@ -36,6 +35,8 @@ namespace osu.Game.Rulesets.Taiko.UI public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager; + protected new TaikoPlayfieldAdjustmentContainer PlayfieldAdjustmentContainer => (TaikoPlayfieldAdjustmentContainer)base.PlayfieldAdjustmentContainer; + protected override bool UserScrollSpeedAdjustment => false; private SkinnableDrawable scroller; @@ -68,22 +69,7 @@ namespace osu.Game.Rulesets.Taiko.UI TimeRange.Value = ComputeTimeRange(); } - protected virtual double ComputeTimeRange() - { - // Taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened. - const float scroll_rate = 10; - - // Since the time range will depend on a positional value, it is referenced to the x480 pixel space. - // Width is used because it defines how many notes fit on the playfield. - // We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default. - float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT); - - // Stable internally increased the slider velocity of objects by a factor of `VELOCITY_MULTIPLIER`. - // To simulate this, we shrink the time range by that factor here. - // This, when combined with the rest of the scrolling ruleset machinery (see `MultiplierControlPoint` et al.), - // has the effect of increasing each multiplier control point's multiplier by `VELOCITY_MULTIPLIER`, ensuring parity with stable. - return (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate / TaikoBeatmapConverter.VELOCITY_MULTIPLIER; - } + protected virtual double ComputeTimeRange() => PlayfieldAdjustmentContainer.ComputeTimeRange(); protected override void UpdateAfterChildren() { diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs index c10e505f50..c67f61052c 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.UI; using osuTK; @@ -14,6 +15,8 @@ namespace osu.Game.Rulesets.Taiko.UI public const float MAXIMUM_ASPECT = 16f / 9f; public const float MINIMUM_ASPECT = 5f / 4f; + private const float stable_gamefield_height = 480f; + public readonly IBindable LockPlayfieldAspectRange = new BindableBool(true); public TaikoPlayfieldAdjustmentContainer() @@ -21,6 +24,9 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.X; RelativePositionAxes = Axes.Y; Height = TaikoPlayfield.BASE_HEIGHT; + + // Matches stable, see https://github.com/peppy/osu-stable-reference/blob/7519cafd1823f1879c0d9c991ba0e5c7fd3bfa02/osu!/GameModes/Play/Rulesets/Taiko/RulesetTaiko.cs#L514 + Y = 135f / stable_gamefield_height; } protected override void Update() @@ -28,8 +34,6 @@ namespace osu.Game.Rulesets.Taiko.UI base.Update(); const float base_relative_height = TaikoPlayfield.BASE_HEIGHT / 768; - // Matches stable, see https://github.com/peppy/osu-stable-reference/blob/7519cafd1823f1879c0d9c991ba0e5c7fd3bfa02/osu!/GameModes/Play/Rulesets/Taiko/RulesetTaiko.cs#L514 - const float base_position = 135f / 480f; float relativeHeight = base_relative_height; @@ -51,10 +55,38 @@ namespace osu.Game.Rulesets.Taiko.UI // Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions. relativeHeight = Math.Min(relativeHeight, 1f / 3f); - Y = base_position; - Scale = new Vector2(Math.Max((Parent!.ChildSize.Y / 768f) * (relativeHeight / base_relative_height), 1f)); Width = 1 / Scale.X; } + + public double ComputeTimeRange() + { + float currentAspect = Parent!.ChildSize.X / Parent!.ChildSize.Y; + + if (LockPlayfieldAspectRange.Value) + currentAspect = Math.Clamp(currentAspect, MINIMUM_ASPECT, MAXIMUM_ASPECT); + + // in a game resolution of 1024x768, stable's scrolling system consists of objects being placed 600px (widthScaled - 40) away from their hit location. + // however, the point at which the object renders at the end of the screen is exactly x=640, but stable makes the object start moving from beyond the screen instead of the boundary point. + // therefore, in lazer we have to adjust the "in length" so that it's in a 640px->160px fashion before passing it down as a "time range". + // see stable's "in length": https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/GameplayElements/HitObjectManagerTaiko.cs#L168 + const float stable_hit_location = 160f; + float widthScaled = currentAspect * stable_gamefield_height; + float inLength = widthScaled - stable_hit_location; + + // also in a game resolution of 1024x768, stable makes hit objects scroll from 760px->160px at a duration of 6000ms, divided by slider velocity (i.e. at a rate of 0.1px/ms) + // compare: https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/GameplayElements/HitObjectManagerTaiko.cs#L218 + // note: the variable "sv", in the linked reference, is equivalent to MultiplierControlPoint.Multiplier * 100, but since time range is agnostic of velocity, we replace "sv" with 100 below. + float inMsLength = inLength / 100 * 1000; + + // stable multiplies the slider velocity by 1.4x for certain reasons, divide the time range by that factor to achieve similar result. + // for references on how the factor is applied to the time range, see: + // 1. https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/GameplayElements/HitObjectManagerTaiko.cs#L79 (DifficultySliderMultiplier multiplied by 1.4x) + // 2. https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/GameplayElements/HitObjectManager.cs#L468-L470 (DifficultySliderMultiplier used to calculate SliderScoringPointDistance) + // 3. https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/GameplayElements/HitObjectManager.cs#L248-L250 (SliderScoringPointDistance used to calculate slider velocity, i.e. the "sv" variable from above) + inMsLength /= TaikoBeatmapConverter.VELOCITY_MULTIPLIER; + + return inMsLength; + } } } From acc2614090739ce546bead00deef3ddcfbbeadf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Jan 2024 20:04:18 +0100 Subject: [PATCH 62/93] Apply alternative solution Fixes the logo flying out of the wrong corner when transitioning from song select to gameplay. Co-authored-by: Dean Herbert --- osu.Game/Screens/Menu/OsuLogo.cs | 7 +++++++ osu.Game/Screens/OsuScreen.cs | 5 ++++- osu.Game/Screens/Select/SongSelect.cs | 3 +-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 25101730e7..f2e2e25fa6 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -486,5 +486,12 @@ namespace osu.Game.Screens.Menu defaultProxyTarget.Add(this); defaultProxyTarget.Add(proxy = CreateProxy()); } + + public void ChangeAnchor(Anchor anchor) + { + var previousAnchor = AnchorPosition; + Anchor = anchor; + Position -= AnchorPosition - previousAnchor; + } } } diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index f719ef67c9..2e8f85423d 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -250,9 +250,12 @@ namespace osu.Game.Screens { logo.Action = null; logo.FadeOut(300, Easing.OutQuint); - logo.Anchor = Anchor.TopLeft; + logo.Origin = Anchor.Centre; + + logo.ChangeAnchor(Anchor.TopLeft); logo.RelativePositionAxes = Axes.Both; + logo.Triangles = true; logo.Ripple = true; } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 50467bc089..a603934a9d 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -642,9 +642,8 @@ namespace osu.Game.Screens.Select { base.LogoArriving(logo, resuming); - logo.Anchor = Anchor.BottomRight; - logo.Origin = Anchor.Centre; logo.RelativePositionAxes = Axes.None; + logo.ChangeAnchor(Anchor.BottomRight); Vector2 position = new Vector2(-76, -36); From 6888cda02cd127ff4baf6159d3c6a7e533184970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Jan 2024 21:42:38 +0100 Subject: [PATCH 63/93] Make `LegacyScoreDecoder.PopulateMaximumStatistics()` public For `osu-tools` consumption. --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index e51a95798b..65e2c02655 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -180,7 +180,7 @@ namespace osu.Game.Scoring.Legacy /// /// The score to populate the statistics of. /// The corresponding . - internal static void PopulateMaximumStatistics(ScoreInfo score, WorkingBeatmap workingBeatmap) + public static void PopulateMaximumStatistics(ScoreInfo score, WorkingBeatmap workingBeatmap) { Debug.Assert(score.BeatmapInfo != null); From 87f853fcd270c9fce2f3f5a7368bfb506d060a23 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 30 Jan 2024 00:23:32 +0300 Subject: [PATCH 64/93] Reduce overhead in ScrollingHitObjectContainer --- .../Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 129918da14..39ddb5c753 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -184,9 +184,12 @@ namespace osu.Game.Rulesets.UI.Scrolling // We need to calculate hit object positions (including nested hit objects) as soon as possible after lifetimes // to prevent hit objects displayed in a wrong position for one frame. - // Only AliveObjects need to be considered for layout (reduces overhead in the case of scroll speed changes). - foreach (var obj in AliveObjects) + // Only AliveEntries need to be considered for layout (reduces overhead in the case of scroll speed changes). + // We are not using AliveObjects directly to avoid selection/sorting overhead since we don't care about the order at which positions will be updated. + foreach (var entry in AliveEntries) { + var obj = entry.Drawable; + updatePosition(obj, Time.Current); if (layoutComputed.Contains(obj)) From 7dba870518857bb832dbf7b4a034471a3ed45f94 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 30 Jan 2024 03:07:37 +0300 Subject: [PATCH 65/93] Rework Content storage in ColumnFlow --- osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs | 4 ++-- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 16 +++++++++------- osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs | 10 +++++----- osu.Game.Rulesets.Mania/UI/Stage.cs | 11 +++++------ 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs index fee3ba3e39..db04142915 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs @@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Mania.Tests { foreach (var stage in stages) { - for (int i = 0; i < stage.Columns.Count; i++) + for (int i = 0; i < stage.Columns.Length; i++) { var obj = new Note { Column = i, StartTime = Time.Current + 2000 }; obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Mania.Tests { foreach (var stage in stages) { - for (int i = 0; i < stage.Columns.Count; i++) + for (int i = 0; i < stage.Columns.Length; i++) { var obj = new HoldNote { Column = i, StartTime = Time.Current + 2000, Duration = 500 }; obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index 8734f8ac8a..1593e8e76f 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -4,8 +4,6 @@ #nullable disable using System; -using System.Collections.Generic; -using System.Linq; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -28,20 +26,21 @@ namespace osu.Game.Rulesets.Mania.UI /// /// All contents added to this . /// - public IReadOnlyList Content => columns.Children.Select(c => c.Count == 0 ? null : (TContent)c.Child).ToList(); + public TContent[] Content { get; } - private readonly FillFlowContainer columns; + private readonly FillFlowContainer> columns; private readonly StageDefinition stageDefinition; public ColumnFlow(StageDefinition stageDefinition) { this.stageDefinition = stageDefinition; + Content = new TContent[stageDefinition.Columns]; AutoSizeAxes = Axes.X; Masking = true; - InternalChild = columns = new FillFlowContainer + InternalChild = columns = new FillFlowContainer> { RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, @@ -49,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.UI }; for (int i = 0; i < stageDefinition.Columns; i++) - columns.Add(new Container { RelativeSizeAxes = Axes.Y }); + columns.Add(new Container { RelativeSizeAxes = Axes.Y }); } private ISkinSource currentSkin; @@ -102,7 +101,10 @@ namespace osu.Game.Rulesets.Mania.UI /// /// The index of the column to set the content of. /// The content. - public void SetContentForColumn(int column, TContent content) => columns[column].Child = content; + public void SetContentForColumn(int column, TContent content) + { + Content[column] = columns[column].Child = content; + } private void updateMobileSizing() { diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index 314d199944..c8874c8ab3 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Mania.UI stages.Add(newStage); AddNested(newStage); - firstColumnIndex += newStage.Columns.Count; + firstColumnIndex += newStage.Columns.Length; } } @@ -125,9 +125,9 @@ namespace osu.Game.Rulesets.Mania.UI foreach (var stage in stages) { - if (index >= stage.Columns.Count) + if (index >= stage.Columns.Length) { - index -= stage.Columns.Count; + index -= stage.Columns.Length; continue; } @@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Mania.UI /// /// Retrieves the total amount of columns across all stages in this playfield. /// - public int TotalColumns => stages.Sum(s => s.Columns.Count); + public int TotalColumns => stages.Sum(s => s.Columns.Length); private Stage getStageByColumn(int column) { @@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Mania.UI foreach (var stage in stages) { - sum += stage.Columns.Count; + sum += stage.Columns.Length; if (sum > column) return stage; } diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 36286940a8..db06f9dbde 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; @@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.UI public const float HIT_TARGET_POSITION = 110; - public IReadOnlyList Columns => columnFlow.Content; + public Column[] Columns => columnFlow.Content; private readonly ColumnFlow columnFlow; private readonly JudgementContainer judgements; @@ -184,13 +183,13 @@ namespace osu.Game.Rulesets.Mania.UI NewResult += OnNewResult; } - public override void Add(HitObject hitObject) => Columns.ElementAt(((ManiaHitObject)hitObject).Column - firstColumnIndex).Add(hitObject); + public override void Add(HitObject hitObject) => Columns[((ManiaHitObject)hitObject).Column - firstColumnIndex].Add(hitObject); - public override bool Remove(HitObject hitObject) => Columns.ElementAt(((ManiaHitObject)hitObject).Column - firstColumnIndex).Remove(hitObject); + public override bool Remove(HitObject hitObject) => Columns[((ManiaHitObject)hitObject).Column - firstColumnIndex].Remove(hitObject); - public override void Add(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Add(h); + public override void Add(DrawableHitObject h) => Columns[((ManiaHitObject)h.HitObject).Column - firstColumnIndex].Add(h); - public override bool Remove(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Remove(h); + public override bool Remove(DrawableHitObject h) => Columns[((ManiaHitObject)h.HitObject).Column - firstColumnIndex].Remove(h); public void Add(BarLine barLine) => base.Add(barLine); From 8e20eed4ef2783234e54101fd0fa8bd92d5e11fe Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 30 Jan 2024 03:19:27 +0300 Subject: [PATCH 66/93] Don't use LINQ in ReceivePositionalInputAt --- osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs | 11 ++++++++++- osu.Game.Rulesets.Mania/UI/Stage.cs | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index c8874c8ab3..0d36f51943 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -42,7 +42,16 @@ namespace osu.Game.Rulesets.Mania.UI } } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos)); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + foreach (var s in stages) + { + if (s.ReceivePositionalInputAt(screenSpacePos)) + return true; + } + + return false; + } public ManiaPlayfield(List stageDefinitions) { diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index db06f9dbde..a4a09c9a82 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -44,7 +44,16 @@ namespace osu.Game.Rulesets.Mania.UI private readonly Drawable barLineContainer; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Columns.Any(c => c.ReceivePositionalInputAt(screenSpacePos)); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + foreach (var c in Columns) + { + if (c.ReceivePositionalInputAt(screenSpacePos)) + return true; + } + + return false; + } private readonly int firstColumnIndex; From d895a91cd535328e327752d98576e5d5db9b20ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Jan 2024 18:40:17 +0900 Subject: [PATCH 67/93] Update endpoints to final production endpoints --- osu.Game/Online/EndpointConfiguration.cs | 16 +++++++--------- .../ExperimentalEndpointConfiguration.cs | 19 ------------------- osu.Game/OsuGameBase.cs | 2 +- 3 files changed, 8 insertions(+), 29 deletions(-) delete mode 100644 osu.Game/Online/ExperimentalEndpointConfiguration.cs diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs index f3bcced630..bd3c945124 100644 --- a/osu.Game/Online/EndpointConfiguration.cs +++ b/osu.Game/Online/EndpointConfiguration.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 - namespace osu.Game.Online { /// @@ -13,36 +11,36 @@ namespace osu.Game.Online /// /// The base URL for the website. /// - public string WebsiteRootUrl { get; set; } + public string WebsiteRootUrl { get; set; } = string.Empty; /// /// The endpoint for the main (osu-web) API. /// - public string APIEndpointUrl { get; set; } + public string APIEndpointUrl { get; set; } = string.Empty; /// /// The OAuth client secret. /// - public string APIClientSecret { get; set; } + public string APIClientSecret { get; set; } = string.Empty; /// /// The OAuth client ID. /// - public string APIClientID { get; set; } + public string APIClientID { get; set; } = string.Empty; /// /// The endpoint for the SignalR spectator server. /// - public string SpectatorEndpointUrl { get; set; } + public string SpectatorEndpointUrl { get; set; } = string.Empty; /// /// The endpoint for the SignalR multiplayer server. /// - public string MultiplayerEndpointUrl { get; set; } + public string MultiplayerEndpointUrl { get; set; } = string.Empty; /// /// The endpoint for the SignalR metadata server. /// - public string MetadataEndpointUrl { get; set; } + public string MetadataEndpointUrl { get; set; } = string.Empty; } } diff --git a/osu.Game/Online/ExperimentalEndpointConfiguration.cs b/osu.Game/Online/ExperimentalEndpointConfiguration.cs deleted file mode 100644 index c3d0014c8b..0000000000 --- a/osu.Game/Online/ExperimentalEndpointConfiguration.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Online -{ - public class ExperimentalEndpointConfiguration : EndpointConfiguration - { - public ExperimentalEndpointConfiguration() - { - WebsiteRootUrl = @"https://osu.ppy.sh"; - APIEndpointUrl = @"https://lazer.ppy.sh"; - APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"; - APIClientID = "5"; - SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator"; - MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer"; - MetadataEndpointUrl = "https://spectator.ppy.sh/metadata"; - } - } -} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 2208f7d7ca..a2a6322665 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -102,7 +102,7 @@ namespace osu.Game public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild; public virtual EndpointConfiguration CreateEndpoints() => - UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ExperimentalEndpointConfiguration(); + UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration(); public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version(); From 6931af664ed3c9a1550a9e2751cfe436b1dc542d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Jan 2024 12:05:31 +0100 Subject: [PATCH 68/93] Add missing `icon.png` entry to nuspec This is only half of the deployment fix, the other half will be in `osu-deploy` (making sure the icon is actually in the staging directory). --- osu.Desktop/osu.nuspec | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec index 3b7d6cbe79..f85698680e 100644 --- a/osu.Desktop/osu.nuspec +++ b/osu.Desktop/osu.nuspec @@ -20,5 +20,6 @@ + From 000ddc14acadf104af4724731c0aa41e27952ab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Jan 2024 20:50:00 +0100 Subject: [PATCH 69/93] Fix broken locking in `OAuth` Closes https://github.com/ppy/osu/issues/26824... I think? Can be reproduced via something like diff --git a/osu.Game/Online/API/OAuth.cs b/osu.Game/Online/API/OAuth.cs index 485274f349..e6e93ab4c7 100644 --- a/osu.Game/Online/API/OAuth.cs +++ b/osu.Game/Online/API/OAuth.cs @@ -151,6 +151,11 @@ internal string RequestAccessToken() { if (!ensureAccessToken()) return null; + for (int i = 0; i < 10000; ++i) + { + _ = Token.Value.AccessToken; + } + return Token.Value.AccessToken; } The cause is `SecondFactorAuthForm` calling `Logout()`, which calls `OAuth.Clear()`, _while_ the `APIAccess` connect loop is checking if `authentication.HasValidAccessToken` is true, which happens to internally check `Token.Value.AccessToken`, which the clearing of tokens can brutally interrupt. --- osu.Game/Online/API/OAuth.cs | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/osu.Game/Online/API/OAuth.cs b/osu.Game/Online/API/OAuth.cs index 485274f349..4829310870 100644 --- a/osu.Game/Online/API/OAuth.cs +++ b/osu.Game/Online/API/OAuth.cs @@ -128,19 +128,12 @@ namespace osu.Game.Online.API // if we already have a valid access token, let's use it. if (accessTokenValid) return true; - // we want to ensure only a single authentication update is happening at once. - lock (access_token_retrieval_lock) - { - // re-check if valid, in case another request completed and revalidated our access. - if (accessTokenValid) return true; + // if not, let's try using our refresh token to request a new access token. + if (!string.IsNullOrEmpty(Token.Value?.RefreshToken)) + // ReSharper disable once PossibleNullReferenceException + AuthenticateWithRefresh(Token.Value.RefreshToken); - // if not, let's try using our refresh token to request a new access token. - if (!string.IsNullOrEmpty(Token.Value?.RefreshToken)) - // ReSharper disable once PossibleNullReferenceException - AuthenticateWithRefresh(Token.Value.RefreshToken); - - return accessTokenValid; - } + return accessTokenValid; } private bool accessTokenValid => Token.Value?.IsValid ?? false; @@ -149,14 +142,18 @@ namespace osu.Game.Online.API internal string RequestAccessToken() { - if (!ensureAccessToken()) return null; + lock (access_token_retrieval_lock) + { + if (!ensureAccessToken()) return null; - return Token.Value.AccessToken; + return Token.Value.AccessToken; + } } internal void Clear() { - Token.Value = null; + lock (access_token_retrieval_lock) + Token.Value = null; } private class AccessTokenRequestRefresh : AccessTokenRequest From 4126dcbe281c2f6590b999af14e27a09748bbde6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Jan 2024 21:40:23 +0100 Subject: [PATCH 70/93] Fix 2FA verification via link not working correctly Closes https://github.com/ppy/osu/issues/26835. I must have not re-tested this correctly after all the refactors... Basically the issue is that the websocket connection would only come online when the API state changed to full `Online`. In particular the connector would not attempt to connect when the API state was `RequiresSecondFactorAuth`, giving the link-based flow no chance to actually work. The change in `WebSocketNotificationsClientConnector` is relevant in that queueing requests does nothing before the API state changes to full `Online`. It also cleans up things a bit code-wise so... win? And yes, this means that the _other_ `PersistentEndpointClientConnector` implementations (i.e. SignalR connectors) will also come online earlier after this. Based on previous discussions (https://github.com/ppy/osu/pull/25480#discussion_r1395566545) I think this is fine, but if it is _not_ fine, then it can be fixed by exposing a virtual that lets a connector to decide when to come alive, I guess. --- .../WebSocketNotificationsClientConnector.cs | 11 ++++------- osu.Game/Online/PersistentEndpointClientConnector.cs | 3 ++- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs index 73fe29d441..596322d377 100644 --- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs +++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs @@ -29,14 +29,11 @@ namespace osu.Game.Online.Notifications.WebSocket protected override async Task BuildConnectionAsync(CancellationToken cancellationToken) { - var tcs = new TaskCompletionSource(); - var req = new GetNotificationsRequest(); - req.Success += bundle => tcs.SetResult(bundle.Endpoint); - req.Failure += ex => tcs.SetException(ex); - api.Queue(req); - - string endpoint = await tcs.Task.ConfigureAwait(false); + // must use `PerformAsync()`, since we may not be fully online yet + // (see `APIState.RequiresSecondFactorAuth` - in this state queued requests will not execute). + await api.PerformAsync(req).ConfigureAwait(false); + string endpoint = req.Response!.Endpoint; ClientWebSocket socket = new ClientWebSocket(); socket.Options.SetRequestHeader(@"Authorization", @$"Bearer {api.AccessToken}"); diff --git a/osu.Game/Online/PersistentEndpointClientConnector.cs b/osu.Game/Online/PersistentEndpointClientConnector.cs index 8c1b58a750..024a0fea73 100644 --- a/osu.Game/Online/PersistentEndpointClientConnector.cs +++ b/osu.Game/Online/PersistentEndpointClientConnector.cs @@ -69,6 +69,7 @@ namespace osu.Game.Online break; case APIState.Online: + case APIState.RequiresSecondFactorAuth: await connect().ConfigureAwait(true); break; } @@ -83,7 +84,7 @@ namespace osu.Game.Online try { - while (apiState.Value == APIState.Online) + while (apiState.Value == APIState.RequiresSecondFactorAuth || apiState.Value == APIState.Online) { // ensure any previous connection was disposed. // this will also create a new cancellation token source. From c5e118bd1011a3a2f9b5f1affa832dc2e0735c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Jan 2024 22:15:35 +0100 Subject: [PATCH 71/93] Revert "Move fade more local to avoid fading twice" This reverts commit d0421fe20667530bf1bca1a5c8e3f387dde0cf6a and fixes https://github.com/ppy/osu/issues/26801. https://github.com/ppy/osu/pull/26703#discussion_r1469409667 was correct in saying that the early fade-out needs to be restored, and that's because of the early-return. Legacy judgements that are the temporary displayed judgement from new piece should also receive the fade-out, and d0421fe20667530bf1bca1a5c8e3f387dde0cf6a broke that. --- osu.Game/Skinning/LegacyJudgementPieceOld.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Skinning/LegacyJudgementPieceOld.cs b/osu.Game/Skinning/LegacyJudgementPieceOld.cs index edfb5a23ec..068707ba4f 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceOld.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceOld.cs @@ -46,6 +46,7 @@ namespace osu.Game.Skinning const double fade_out_length = 600; this.FadeInFromZero(fade_in_length); + this.Delay(fade_out_delay).FadeOut(fade_out_length); // legacy judgements don't play any transforms if they are an animation.... UNLESS they are the temporary displayed judgement from new piece. if (animation?.FrameCount > 1 && !forceTransforms) @@ -79,8 +80,6 @@ namespace osu.Game.Skinning this.RotateTo(0); this.RotateTo(rotation, fade_in_length) .Then().RotateTo(rotation * 2, fade_out_delay + fade_out_length - fade_in_length, Easing.In); - - this.Delay(fade_out_delay).FadeOut(fade_out_length); } } else @@ -94,8 +93,6 @@ namespace osu.Game.Skinning // so we need to force the current value to be correct at 1.2 (0.95) then complete the // second half of the transform. .ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f); // t = 1.4 - - this.Delay(fade_out_delay).FadeOut(fade_out_length); } } From 9b1bbe5f48eaaf6854c6055d45d6c4601ff3226e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 31 Jan 2024 15:54:43 +0900 Subject: [PATCH 72/93] Adjust default min result of SliderTailHit, remove override --- osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs | 1 - osu.Game/Rulesets/Judgements/Judgement.cs | 4 +++- osu.Game/Rulesets/Scoring/HitResult.cs | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index ceee513412..ee2490439f 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -30,7 +30,6 @@ namespace osu.Game.Rulesets.Osu.Objects public class TailJudgement : SliderEndJudgement { public override HitResult MaxResult => HitResult.SliderTailHit; - public override HitResult MinResult => HitResult.IgnoreMiss; } } } diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index 93386de483..d4d06167f1 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -73,9 +73,11 @@ namespace osu.Game.Rulesets.Judgements return HitResult.SmallTickMiss; case HitResult.LargeTickHit: - case HitResult.SliderTailHit: return HitResult.LargeTickMiss; + case HitResult.SliderTailHit: + return HitResult.IgnoreMiss; + default: return HitResult.Miss; } diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 20ec3c4946..b6cfca58db 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -138,7 +138,8 @@ namespace osu.Game.Rulesets.Scoring ComboBreak, /// - /// A special judgement similar to that's used to increase the valuation of the final tick of a slider. + /// A special tick judgement to increase the valuation of the final tick of a slider. + /// The default minimum result is , but may be overridden to . /// [EnumMember(Value = "slider_tail_hit")] [Order(8)] From 3f527081bd663064cf098b5b8af003ba4d41b2ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Jan 2024 16:12:46 +0900 Subject: [PATCH 73/93] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 55ef55ab7d..d944e2ce8e 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 5b99319499..bd6891f448 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From a7f9f50ce5c3924733c82934fc0402af41d83802 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Jan 2024 16:52:50 +0900 Subject: [PATCH 74/93] Show a better message when score submission fails due to system clock being set wrong --- osu.Game/Screens/Play/SubmittingPlayer.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 171ceea84f..c8e84f1961 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -132,7 +132,18 @@ namespace osu.Game.Screens.Play if (string.IsNullOrEmpty(exception.Message)) Logger.Error(exception, "Failed to retrieve a score submission token."); else - Logger.Log($"You are not able to submit a score: {exception.Message}", level: LogLevel.Important); + { + switch (exception.Message) + { + case "expired token": + Logger.Log("Score submission failed because your system clock is set incorrectly. Please check your system time, date and timezone.", level: LogLevel.Important); + break; + + default: + Logger.Log($"You are not able to submit a score: {exception.Message}", level: LogLevel.Important); + break; + } + } Schedule(() => { From ebf637bd3c33f1c886f6bfc81aa9ea2132c9e0d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Jan 2024 17:41:26 +0900 Subject: [PATCH 75/93] Adjust slider tick / end misses to show slightly longer --- .../Skinning/Argon/ArgonJudgementPieceSliderTickMiss.cs | 2 +- .../Skinning/Default/DefaultJudgementPieceSliderTickMiss.cs | 2 +- osu.Game/Skinning/LegacyJudgementPieceOld.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPieceSliderTickMiss.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPieceSliderTickMiss.cs index 878e8dbfc2..bd883d6e4c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPieceSliderTickMiss.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPieceSliderTickMiss.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon this.ScaleTo(1.4f); this.ScaleTo(1f, 150, Easing.Out); - this.FadeOutFromOne(400); + this.FadeOutFromOne(600); } public Drawable? GetAboveHitObjectsProxiedContent() => piece.CreateProxy(); diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultJudgementPieceSliderTickMiss.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultJudgementPieceSliderTickMiss.cs index 9fc71852ba..04c15a1433 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultJudgementPieceSliderTickMiss.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultJudgementPieceSliderTickMiss.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default this.ScaleTo(1.4f); this.ScaleTo(1f, 150, Easing.Out); - this.FadeOutFromOne(400); + this.FadeOutFromOne(600); } public Drawable? GetAboveHitObjectsProxiedContent() => piece.CreateProxy(); diff --git a/osu.Game/Skinning/LegacyJudgementPieceOld.cs b/osu.Game/Skinning/LegacyJudgementPieceOld.cs index 068707ba4f..c8630b54a6 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceOld.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceOld.cs @@ -62,7 +62,7 @@ namespace osu.Game.Skinning this.ScaleTo(1.2f); this.ScaleTo(1f, 100, Easing.In); - this.FadeOutFromOne(400); + this.Delay(fade_out_delay / 2).FadeOut(fade_out_length); } else { From fbc923b47ed5e00ac94b5fa20b7310208e650e8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Jan 2024 17:51:38 +0900 Subject: [PATCH 76/93] Revert "Merge pull request #26870 from smoogipoo/adjust-default-minresult" This reverts commit 1acff746ee65020689b873c279aefb9c6c3d8124, reversing changes made to 696ecda398b22da06066bb4d5fc32861758829a8. --- osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs | 1 + osu.Game/Rulesets/Judgements/Judgement.cs | 4 +--- osu.Game/Rulesets/Scoring/HitResult.cs | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index ee2490439f..ceee513412 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -30,6 +30,7 @@ namespace osu.Game.Rulesets.Osu.Objects public class TailJudgement : SliderEndJudgement { public override HitResult MaxResult => HitResult.SliderTailHit; + public override HitResult MinResult => HitResult.IgnoreMiss; } } } diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index d4d06167f1..93386de483 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -73,10 +73,8 @@ namespace osu.Game.Rulesets.Judgements return HitResult.SmallTickMiss; case HitResult.LargeTickHit: - return HitResult.LargeTickMiss; - case HitResult.SliderTailHit: - return HitResult.IgnoreMiss; + return HitResult.LargeTickMiss; default: return HitResult.Miss; diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index b6cfca58db..20ec3c4946 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -138,8 +138,7 @@ namespace osu.Game.Rulesets.Scoring ComboBreak, /// - /// A special tick judgement to increase the valuation of the final tick of a slider. - /// The default minimum result is , but may be overridden to . + /// A special judgement similar to that's used to increase the valuation of the final tick of a slider. /// [EnumMember(Value = "slider_tail_hit")] [Order(8)] From f927cb59285ff730f7c8ad7426775f992c0f1352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 31 Jan 2024 12:29:18 +0100 Subject: [PATCH 77/93] Increase repeat count for better coverage of flip operations --- .../OsuHitObjectGenerationUtilsTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs index d78c32aa6a..ffb9ad1784 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests new PathControlPoint(new Vector2(-128, 0), PathType.LINEAR) // absolute position: (0, 128) } }, - RepeatCount = 1 + RepeatCount = 2 }; slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); return slider; @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Tests OsuHitObjectGenerationUtils.ReflectHorizontallyAlongPlayfield(slider); Assert.That(slider.Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 128, 128))); - Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 0, 128))); + Assert.That(slider.NestedHitObjects.OfType().First().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 0, 128))); Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[] { new Vector2(), @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Tests OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(slider); Assert.That(slider.Position, Is.EqualTo(new Vector2(128, OsuPlayfield.BASE_SIZE.Y - 128))); - Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(0, OsuPlayfield.BASE_SIZE.Y - 128))); + Assert.That(slider.NestedHitObjects.OfType().First().Position, Is.EqualTo(new Vector2(0, OsuPlayfield.BASE_SIZE.Y - 128))); Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[] { new Vector2(), @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Tests OsuHitObjectGenerationUtils.FlipSliderInPlaceHorizontally(slider); Assert.That(slider.Position, Is.EqualTo(new Vector2(128, 128))); - Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(256, 128))); + Assert.That(slider.NestedHitObjects.OfType().First().Position, Is.EqualTo(new Vector2(256, 128))); Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[] { new Vector2(), From a934556bb8e4d266b67c35bbc8c328d540ee35cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 31 Jan 2024 12:34:38 +0100 Subject: [PATCH 78/93] Add failing assertions for head/tail positions after flip --- .../OsuHitObjectGenerationUtilsTest.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs index ffb9ad1784..77ef4627cb 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs @@ -45,7 +45,9 @@ namespace osu.Game.Rulesets.Osu.Tests OsuHitObjectGenerationUtils.ReflectHorizontallyAlongPlayfield(slider); Assert.That(slider.Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 128, 128))); + Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 128, 128))); Assert.That(slider.NestedHitObjects.OfType().First().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 0, 128))); + Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X, 128))); Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[] { new Vector2(), @@ -62,7 +64,9 @@ namespace osu.Game.Rulesets.Osu.Tests OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(slider); Assert.That(slider.Position, Is.EqualTo(new Vector2(128, OsuPlayfield.BASE_SIZE.Y - 128))); + Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(128, OsuPlayfield.BASE_SIZE.Y - 128))); Assert.That(slider.NestedHitObjects.OfType().First().Position, Is.EqualTo(new Vector2(0, OsuPlayfield.BASE_SIZE.Y - 128))); + Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(0, OsuPlayfield.BASE_SIZE.Y - 128))); Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[] { new Vector2(), @@ -79,7 +83,9 @@ namespace osu.Game.Rulesets.Osu.Tests OsuHitObjectGenerationUtils.FlipSliderInPlaceHorizontally(slider); Assert.That(slider.Position, Is.EqualTo(new Vector2(128, 128))); + Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(128, 128))); Assert.That(slider.NestedHitObjects.OfType().First().Position, Is.EqualTo(new Vector2(256, 128))); + Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(256, 128))); Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[] { new Vector2(), From dfea2ade6d7d1dda0b080fca826cec767a480199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 31 Jan 2024 12:43:24 +0100 Subject: [PATCH 79/93] Revert incorrect end position optimisation Closes https://github.com/ppy/osu/issues/26867. Reverts 882f49039029b7dc3e287ccc302d04de89de10df and ce643aa68f35369be1a975bb1ceb69fb54192cf2. The applied optimisation may have been valid as long as it was constrained to `Slider`. But it is not, as `SliderTailCircle` stores a local copy of the object position. And as the commit message of ce643aa68f35369be1a975bb1ceb69fb54192cf2 states, this could be bypassed by some pretty hacky delegation from `SliderTailCircle.Position` to the slider, but it'd also be pretty hacky because it would make flows like `PositionBindable` break down. Long-term solution is to probably remove bindables from hitobjects. --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 032f105ded..506145568e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects set { repeatCount = value; - endPositionCache.Invalidate(); + updateNestedPositions(); } } @@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Objects public Slider() { SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples(); - Path.Version.ValueChanged += _ => endPositionCache.Invalidate(); + Path.Version.ValueChanged += _ => updateNestedPositions(); } protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) From 424859328938f86d6520bf784530c3b33d2a1ffb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Jan 2024 22:41:44 +0900 Subject: [PATCH 80/93] Fix menu banner not updating as often as we want it to --- osu.Game/Online/API/Requests/GetSystemTitleRequest.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs b/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs index 659e46bb11..52ca0c11eb 100644 --- a/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs +++ b/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests @@ -9,7 +8,7 @@ namespace osu.Game.Online.API.Requests public class GetSystemTitleRequest : OsuJsonWebRequest { public GetSystemTitleRequest() - : base($@"https://assets.ppy.sh/lazer-status.json?{DateTimeOffset.UtcNow.ToUnixTimeSeconds() / 1800}") + : base(@"https://assets.ppy.sh/lazer-status.json") { } } From 563f4a26b139be4b4c9354af20b5275527ee85d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Feb 2024 21:10:37 +0900 Subject: [PATCH 81/93] Show "failing" icon on user panel when 2FA prompt is present This gives the user a chance to know it's required. --- osu.Game/Overlays/Toolbar/ToolbarUserButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index 28521e3331..1e812d7238 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -95,7 +95,7 @@ namespace osu.Game.Overlays.Toolbar private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => { - failingIcon.FadeTo(state.NewValue == APIState.Failing ? 1 : 0, 200, Easing.OutQuint); + failingIcon.FadeTo(state.NewValue == APIState.Failing || state.NewValue == APIState.RequiresSecondFactorAuth ? 1 : 0, 200, Easing.OutQuint); switch (state.NewValue) { From bf3746daa8e8117a23978b3982185af7145d37fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Feb 2024 21:10:53 +0900 Subject: [PATCH 82/93] Show login overlay at main menu when 2FA is required --- osu.Game/Screens/Menu/MainMenu.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index a75edd1cff..decb901c32 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -280,7 +280,7 @@ namespace osu.Game.Screens.Menu sideFlashes.Delay(FADE_IN_DURATION).FadeIn(64, Easing.InQuint); } - else if (!api.IsLoggedIn) + else if (!api.IsLoggedIn || api.State.Value == APIState.RequiresSecondFactorAuth) { // copy out old action to avoid accidentally capturing logo.Action in closure, causing a self-reference loop. var previousAction = logo.Action; From 53c5483eba108acb4c8782764ae4840d414428dd Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 2 Feb 2024 04:53:48 +0300 Subject: [PATCH 83/93] Reduce allocation in Playfield --- osu.Game/Rulesets/UI/Playfield.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index e9c35555c8..90a2f63faa 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -247,10 +247,14 @@ namespace osu.Game.Rulesets.UI nestedPlayfields.Add(otherPlayfield); } + private Mod[] mods; + protected override void LoadComplete() { base.LoadComplete(); + mods = Mods?.ToArray(); + // in the case a consumer forgets to add the HitObjectContainer, we will add it here. if (HitObjectContainer.Parent == null) AddInternal(HitObjectContainer); @@ -260,9 +264,9 @@ namespace osu.Game.Rulesets.UI { base.Update(); - if (!IsNested && Mods != null) + if (!IsNested && mods != null) { - foreach (var mod in Mods) + foreach (Mod mod in mods) { if (mod is IUpdatableByPlayfield updatable) updatable.Update(this); @@ -403,10 +407,13 @@ namespace osu.Game.Rulesets.UI // If this is the first time this DHO is being used, then apply the DHO mods. // This is done before Apply() so that the state is updated once when the hitobject is applied. - if (Mods != null) + if (mods != null) { - foreach (var m in Mods.OfType()) - m.ApplyToDrawableHitObject(dho); + foreach (Mod mod in mods) + { + if (mod is IApplicableToDrawableHitObject applicable) + applicable.ApplyToDrawableHitObject(dho); + } } } From 2ff46daf5ede3204da4263fa265ee417601b667a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 2 Feb 2024 06:34:47 +0100 Subject: [PATCH 84/93] Also change icon and tooltip text when pending 2FA --- osu.Game/Localisation/ToolbarStrings.cs | 5 +++++ osu.Game/Overlays/Toolbar/ToolbarUserButton.cs | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/ToolbarStrings.cs b/osu.Game/Localisation/ToolbarStrings.cs index e71a3fff9b..5822f76e02 100644 --- a/osu.Game/Localisation/ToolbarStrings.cs +++ b/osu.Game/Localisation/ToolbarStrings.cs @@ -19,6 +19,11 @@ namespace osu.Game.Localisation /// public static LocalisableString Connecting => new TranslatableString(getKey(@"connecting"), @"Connecting..."); + /// + /// "Verification required" + /// + public static LocalisableString VerificationRequired => new TranslatableString(getKey(@"verification_required"), @"Verification required"); + /// /// "home" /// diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index 1e812d7238..2620e850c8 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -99,7 +99,6 @@ namespace osu.Game.Overlays.Toolbar switch (state.NewValue) { - case APIState.RequiresSecondFactorAuth: case APIState.Connecting: TooltipText = ToolbarStrings.Connecting; spinner.Show(); @@ -108,6 +107,13 @@ namespace osu.Game.Overlays.Toolbar case APIState.Failing: TooltipText = ToolbarStrings.AttemptingToReconnect; spinner.Show(); + failingIcon.Icon = FontAwesome.Solid.ExclamationTriangle; + break; + + case APIState.RequiresSecondFactorAuth: + TooltipText = ToolbarStrings.VerificationRequired; + spinner.Show(); + failingIcon.Icon = FontAwesome.Solid.Key; break; case APIState.Offline: From bb6387bea01ebac9e5c3e6c2ffa043892824319b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 2 Feb 2024 06:53:00 +0100 Subject: [PATCH 85/93] Enable NRT in `ScreenEntry` --- .../Overlays/AccountCreation/ScreenEntry.cs | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index 9ad507d82a..64d0fa94ab 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.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 System; using System.Linq; using System.Threading.Tasks; @@ -28,28 +26,28 @@ namespace osu.Game.Overlays.AccountCreation { public partial class ScreenEntry : AccountCreationScreen { - private ErrorTextFlowContainer usernameDescription; - private ErrorTextFlowContainer emailAddressDescription; - private ErrorTextFlowContainer passwordDescription; + private ErrorTextFlowContainer usernameDescription = null!; + private ErrorTextFlowContainer emailAddressDescription = null!; + private ErrorTextFlowContainer passwordDescription = null!; - private OsuTextBox usernameTextBox; - private OsuTextBox emailTextBox; - private OsuPasswordTextBox passwordTextBox; + private OsuTextBox usernameTextBox = null!; + private OsuTextBox emailTextBox = null!; + private OsuPasswordTextBox passwordTextBox = null!; [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; - private ShakeContainer registerShake; - private ITextPart characterCheckText; + private ShakeContainer registerShake = null!; + private ITextPart characterCheckText = null!; - private OsuTextBox[] textboxes; - private LoadingLayer loadingLayer; + private OsuTextBox[] textboxes = null!; + private LoadingLayer loadingLayer = null!; [Resolved] - private GameHost host { get; set; } + private GameHost? host { get; set; } [Resolved] - private OsuGame game { get; set; } + private OsuGame game { get; set; } = null!; [BackgroundDependencyLoader] private void load() @@ -180,7 +178,7 @@ namespace osu.Game.Overlays.AccountCreation Task.Run(() => { bool success; - RegistrationRequest.RegistrationRequestErrors errors = null; + RegistrationRequest.RegistrationRequestErrors? errors = null; try { @@ -241,6 +239,6 @@ namespace osu.Game.Overlays.AccountCreation return false; } - private OsuTextBox nextUnfilledTextBox() => textboxes.FirstOrDefault(t => string.IsNullOrEmpty(t.Text)); + private OsuTextBox? nextUnfilledTextBox() => textboxes.FirstOrDefault(t => string.IsNullOrEmpty(t.Text)); } } From b58ac7950bd69d383cd26e6c3142b4ea568e9a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 2 Feb 2024 06:53:17 +0100 Subject: [PATCH 86/93] Make game dependency in `ScreenEntry` optional to unbreak tests --- osu.Game/Overlays/AccountCreation/ScreenEntry.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index 64d0fa94ab..ab462357c0 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -47,7 +47,7 @@ namespace osu.Game.Overlays.AccountCreation private GameHost? host { get; set; } [Resolved] - private OsuGame game { get; set; } = null!; + private OsuGame? game { get; set; } [BackgroundDependencyLoader] private void load() @@ -208,7 +208,7 @@ namespace osu.Game.Overlays.AccountCreation if (!string.IsNullOrEmpty(errors.Message)) passwordDescription.AddErrors(new[] { errors.Message }); - game.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}", true); + game?.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}", true); } } else From 93d34e411535f2a47442146f92f62a3cab150821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 2 Feb 2024 06:54:18 +0100 Subject: [PATCH 87/93] Enable NRT in `ScreenWarning` --- osu.Game/Overlays/AccountCreation/ScreenWarning.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs index 0fbf6ba59e..c24bd32bb4 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenWarning.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.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -23,14 +21,14 @@ namespace osu.Game.Overlays.AccountCreation { public partial class ScreenWarning : AccountCreationScreen { - private OsuTextFlowContainer multiAccountExplanationText; - private LinkFlowContainer furtherAssistance; + private OsuTextFlowContainer multiAccountExplanationText = null!; + private LinkFlowContainer furtherAssistance = null!; - [Resolved(canBeNull: true)] - private IAPIProvider api { get; set; } + [Resolved] + private IAPIProvider? api { get; set; } - [Resolved(canBeNull: true)] - private OsuGame game { get; set; } + [Resolved] + private OsuGame? game { get; set; } private const string help_centre_url = "/help/wiki/Help_Centre#login"; From 45f60b035e549e24a9497bf0f11de2eb3d6a4b04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 2 Feb 2024 07:05:27 +0100 Subject: [PATCH 88/93] Enable NRT in `AccountCreationOverlay` --- osu.Game/Overlays/AccountCreationOverlay.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/AccountCreationOverlay.cs b/osu.Game/Overlays/AccountCreationOverlay.cs index 576ee92b48..82fc5508f1 100644 --- a/osu.Game/Overlays/AccountCreationOverlay.cs +++ b/osu.Game/Overlays/AccountCreationOverlay.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 System; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -25,7 +23,9 @@ namespace osu.Game.Overlays { private const float transition_time = 400; - private ScreenWelcome welcomeScreen; + private ScreenWelcome welcomeScreen = null!; + + private ScheduledDelegate? scheduledHide; public AccountCreationOverlay() { @@ -108,8 +108,6 @@ namespace osu.Game.Overlays this.FadeOut(100); } - private ScheduledDelegate scheduledHide; - private void apiStateChanged(ValueChangedEvent state) { switch (state.NewValue) From a00cf87925a06b485d039ba03dcf72fc06127789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 2 Feb 2024 06:50:27 +0100 Subject: [PATCH 89/93] Add 2FA verification screen to registration flow --- .../ScreenEmailVerification.cs | 24 +++++++++++++++++++ .../Overlays/AccountCreation/ScreenEntry.cs | 11 +++++++++ 2 files changed, 35 insertions(+) create mode 100644 osu.Game/Overlays/AccountCreation/ScreenEmailVerification.cs diff --git a/osu.Game/Overlays/AccountCreation/ScreenEmailVerification.cs b/osu.Game/Overlays/AccountCreation/ScreenEmailVerification.cs new file mode 100644 index 0000000000..f3b42117ea --- /dev/null +++ b/osu.Game/Overlays/AccountCreation/ScreenEmailVerification.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.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Overlays.Login; + +namespace osu.Game.Overlays.AccountCreation +{ + public partial class ScreenEmailVerification : AccountCreationScreen + { + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new SecondFactorAuthForm + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(20), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + } +} diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index ab462357c0..f57c7d22a2 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -37,6 +38,8 @@ namespace osu.Game.Overlays.AccountCreation [Resolved] private IAPIProvider api { get; set; } = null!; + private IBindable apiState = null!; + private ShakeContainer registerShake = null!; private ITextPart characterCheckText = null!; @@ -142,6 +145,8 @@ namespace osu.Game.Overlays.AccountCreation passwordTextBox.Current.BindValueChanged(_ => updateCharacterCheckTextColour(), true); characterCheckText.DrawablePartsRecreated += _ => updateCharacterCheckTextColour(); + + apiState = api.State.GetBoundCopy(); } private void updateCharacterCheckTextColour() @@ -221,6 +226,12 @@ namespace osu.Game.Overlays.AccountCreation return; } + apiState.BindValueChanged(state => + { + if (state.NewValue == APIState.RequiresSecondFactorAuth) + this.Push(new ScreenEmailVerification()); + }); + api.Login(usernameTextBox.Text, passwordTextBox.Text); }); }); From 091618db1a81ec017ef0903f2844b95e048465e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 2 Feb 2024 07:04:31 +0100 Subject: [PATCH 90/93] Add test coverage of full account creation flow --- .../Online/TestSceneAccountCreationOverlay.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs index 79fb063ea9..b9d7312233 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.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; @@ -67,5 +68,34 @@ namespace osu.Game.Tests.Visual.Online }); AddUntilStep("overlay is hidden", () => accountCreation.State.Value == Visibility.Hidden); } + + [Test] + public void TestFullFlow() + { + AddStep("log out", () => API.Logout()); + + AddStep("show manually", () => accountCreation.Show()); + AddUntilStep("overlay is visible", () => accountCreation.State.Value == Visibility.Visible); + + AddStep("click button", () => accountCreation.ChildrenOfType().Single().TriggerClick()); + AddUntilStep("warning screen is present", () => accountCreation.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + + AddStep("proceed", () => accountCreation.ChildrenOfType().Single().TriggerClick()); + AddUntilStep("entry screen is present", () => accountCreation.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + + AddStep("input details", () => + { + var entryScreen = accountCreation.ChildrenOfType().Single(); + entryScreen.ChildrenOfType().ElementAt(0).Text = "new_user"; + entryScreen.ChildrenOfType().ElementAt(1).Text = "new.user@fake.mail"; + entryScreen.ChildrenOfType().ElementAt(2).Text = "password"; + }); + AddStep("click button", () => accountCreation.ChildrenOfType().Single() + .ChildrenOfType().Single().TriggerClick()); + AddUntilStep("verification screen is present", () => accountCreation.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + + AddStep("verify", () => ((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh")); + AddUntilStep("overlay is hidden", () => accountCreation.State.Value == Visibility.Hidden); + } } } From b44f77cee1b6adb6568f9b5764382a6cd6ffe2ed Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 2 Feb 2024 19:48:13 +0900 Subject: [PATCH 91/93] Update R# + fix inspections --- .config/dotnet-tools.json | 4 +- .globalconfig | 4 +- .../TestScenePathControlPointVisualiser.cs | 2 +- .../TestSceneFollowPoints.cs | 2 +- .../TestSceneSkinFallbacks.cs | 2 + .../UI/DrumTouchInputArea.cs | 3 +- osu.Game.Tests/Beatmaps/WorkingBeatmapTest.cs | 6 ++- osu.Game.Tests/Database/RulesetStoreTests.cs | 6 +-- .../Gameplay/TestSceneStoryboardSamples.cs | 4 +- .../TestSceneBrokenRulesetHandling.cs | 12 +++--- .../Editing/TestSceneEditorTestGameplay.cs | 4 +- .../Gameplay/TestSceneReplayRecorder.cs | 2 +- .../Gameplay/TestSceneSpectatorPlayback.cs | 2 +- .../Visual/Menus/TestSceneToolbar.cs | 2 +- .../TestSceneLoungeRoomsContainer.cs | 2 +- .../TestScenePlaylistsMatchSettingsOverlay.cs | 2 +- .../TestScenePlaylistsRoomCreation.cs | 8 ++-- .../Visual/Ranking/TestSceneResultsScreen.cs | 2 +- .../TestSceneFirstRunSetupOverlay.cs | 2 +- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 3 +- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 2 +- .../SelectionCycleFillFlowContainer.cs | 4 +- .../UserInterface/BreadcrumbControl.cs | 2 + osu.Game/IO/IStorageResourceProvider.cs | 2 +- osu.Game/Online/Spectator/SpectatorClient.cs | 3 +- osu.Game/Overlays/Comments/VotePill.cs | 2 +- .../Overlays/MedalSplash/DrawableMedal.cs | 2 + osu.Game/Overlays/Volume/VolumeMeter.cs | 2 + .../Objects/Drawables/DrawableHitObject.cs | 3 +- .../Objects/Pooling/HitObjectEntryManager.cs | 5 +-- .../Edit/Compose/Components/DragBox.cs | 2 + osu.Game/Screens/Menu/ButtonArea.cs | 3 ++ .../OnlinePlay/Components/RoomManager.cs | 2 + .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- .../Screens/Play/HUD/HoldForMenuButton.cs | 2 +- .../Multiplayer/MultiplayerTestScene.cs | 4 +- .../IOnlinePlayTestSceneDependencies.cs | 12 +++--- .../Visual/OnlinePlay/OnlinePlayTestScene.cs | 42 +++++++++++-------- .../OnlinePlayTestSceneDependencies.cs | 4 +- osu.Game/Tests/Visual/SkinnableTestScene.cs | 4 +- osu.sln.DotSettings | 5 +++ 41 files changed, 102 insertions(+), 81 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index b8dc201559..99906f0895 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,13 +3,13 @@ "isRoot": true, "tools": { "jetbrains.resharper.globaltools": { - "version": "2022.2.3", + "version": "2023.3.3", "commands": [ "jb" ] }, "nvika": { - "version": "2.2.0", + "version": "3.0.0", "commands": [ "nvika" ] diff --git a/.globalconfig b/.globalconfig index a7b652c454..a4d4707f9b 100644 --- a/.globalconfig +++ b/.globalconfig @@ -1,5 +1,3 @@ -is_global = true - # .NET Code Style # IDE styles reference: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ @@ -56,4 +54,4 @@ dotnet_diagnostic.RS0030.severity = error # Temporarily disable analysing CanBeNull = true in NRT contexts due to mobile issues. # See: https://github.com/ppy/osu/pull/19677 -dotnet_diagnostic.OSUF001.severity = none \ No newline at end of file +dotnet_diagnostic.OSUF001.severity = none diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 8234381283..2b53554ed1 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -206,7 +206,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { AddStep($"click context menu item \"{contextMenuText}\"", () => { - MenuItem item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == "Curve type")?.Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText); + MenuItem item = visualiser.ContextMenuItems!.FirstOrDefault(menuItem => menuItem.Text.Value == "Curve type")?.Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText); item?.Action.Value?.Invoke(); }); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs index eefaa3cae3..28c9d71139 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs @@ -183,7 +183,7 @@ namespace osu.Game.Rulesets.Osu.Tests break; } - hitObjectContainer.Add(drawableObject); + hitObjectContainer.Add(drawableObject!); followPointRenderer.AddFollowPoints(objects[i]); } }); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index 09b906cb10..c624fbbe73 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -173,6 +174,7 @@ namespace osu.Game.Rulesets.Osu.Tests public IEnumerable AllSources => new[] { this }; + [CanBeNull] public event Action SourceChanged; private bool enabled = true; diff --git a/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs b/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs index 29ccd96675..0b7f6f621a 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs @@ -179,10 +179,9 @@ namespace osu.Game.Rulesets.Taiko.UI TaikoAction taikoAction = getTaikoActionFromPosition(position); // Not too sure how this can happen, but let's avoid throwing. - if (trackedActions.ContainsKey(source)) + if (!trackedActions.TryAdd(source, taikoAction)) return; - trackedActions.Add(source, taikoAction); keyBindingContainer.TriggerPressed(taikoAction); } diff --git a/osu.Game.Tests/Beatmaps/WorkingBeatmapTest.cs b/osu.Game.Tests/Beatmaps/WorkingBeatmapTest.cs index f4b1028c0e..3c26f8e39a 100644 --- a/osu.Game.Tests/Beatmaps/WorkingBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/WorkingBeatmapTest.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using Moq; using NUnit.Framework; using osu.Game.Beatmaps; @@ -98,9 +99,10 @@ namespace osu.Game.Tests.Beatmaps Beatmap = beatmap; } +#pragma warning disable CS0067 + [CanBeNull] public event Action> ObjectConverted; - - protected virtual void OnObjectConverted(HitObject arg1, IEnumerable arg2) => ObjectConverted?.Invoke(arg1, arg2); +#pragma warning restore CS0067 public IBeatmap Beatmap { get; } diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index 8b4c6e2411..ddf207342a 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Database Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True); // Availability is updated on construction of a RealmRulesetStore - var _ = new RealmRulesetStore(realm, storage); + _ = new RealmRulesetStore(realm, storage); Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.False); }); @@ -104,13 +104,13 @@ namespace osu.Game.Tests.Database Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True); // Availability is updated on construction of a RealmRulesetStore - var _ = new RealmRulesetStore(realm, storage); + _ = new RealmRulesetStore(realm, storage); Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.False); // Simulate the ruleset getting updated LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION; - var __ = new RealmRulesetStore(realm, storage); + _ = new RealmRulesetStore(realm, storage); Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True); }); diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 4fb9db845b..61161f3206 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -203,9 +203,9 @@ namespace osu.Game.Tests.Gameplay public IRenderer Renderer => host.Renderer; public AudioManager AudioManager => Audio; - public IResourceStore Files => null; + public IResourceStore Files => null!; public new IResourceStore Resources => base.Resources; - public RealmAccess RealmAccess => null; + public RealmAccess RealmAccess => null!; public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null; #endregion diff --git a/osu.Game.Tests/Rulesets/TestSceneBrokenRulesetHandling.cs b/osu.Game.Tests/Rulesets/TestSceneBrokenRulesetHandling.cs index dac6beea65..b378704e80 100644 --- a/osu.Game.Tests/Rulesets/TestSceneBrokenRulesetHandling.cs +++ b/osu.Game.Tests/Rulesets/TestSceneBrokenRulesetHandling.cs @@ -56,9 +56,9 @@ namespace osu.Game.Tests.Rulesets public override IEnumerable GetModsFor(ModType type) => new Mod[] { null }; - public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => null; - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null; - public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null; + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => null!; + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!; + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!; } private class TestAPIIncompatibleRuleset : Ruleset @@ -69,9 +69,9 @@ namespace osu.Game.Tests.Rulesets // simulate API incompatibility by throwing similar exceptions. public override IEnumerable GetModsFor(ModType type) => throw new MissingMethodException(); - public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => null; - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null; - public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null; + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => null!; + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!; + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!; } } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index bbd7123f20..ca5e89c8ed 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -142,7 +142,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("dismiss prompt", () => { - var button = DialogOverlay.CurrentDialog.Buttons.Last(); + var button = DialogOverlay.CurrentDialog!.Buttons.Last(); InputManager.MoveMouseTo(button); InputManager.Click(MouseButton.Left); }); @@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Editing }); AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog); - AddStep("save changes", () => DialogOverlay.CurrentDialog.PerformOkAction()); + AddStep("save changes", () => DialogOverlay.CurrentDialog!.PerformOkAction()); EditorPlayer editorPlayer = null; AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index c51883b221..a7ab021884 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void Update() { base.Update(); - playbackManager?.ReplayInputHandler.SetFrameFromTime(Time.Current - 100); + playbackManager?.ReplayInputHandler?.SetFrameFromTime(Time.Current - 100); } [TearDownSteps] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 3a5b3864af..dd5bbf70b4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -198,7 +198,7 @@ namespace osu.Game.Tests.Visual.Gameplay foreach (var legacyFrame in frames.Frames) { var frame = new TestReplayFrame(); - frame.FromLegacy(legacyFrame, null); + frame.FromLegacy(legacyFrame, null!); playbackReplay.Frames.Add(frame); } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index ce9f80a84f..12d7dde11b 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -250,7 +250,7 @@ namespace osu.Game.Tests.Visual.Menus { } - public virtual IBindable UnreadCount => null; + public virtual IBindable UnreadCount { get; } = new Bindable(); public IEnumerable AllNotifications => Enumerable.Empty(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index d99d764449..b938e59d63 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer .SkipWhile(r => r.Room.Category.Value == RoomCategory.Spotlight) .All(r => r.Room.Category.Value == RoomCategory.Normal)); - AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.FirstOrDefault(r => r.RoomID.Value == 0))); + AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.First(r => r.RoomID.Value == 0))); AddAssert("has 4 rooms", () => container.Rooms.Count == 4); AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0)); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index 1053789b27..9f7b20ad43 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -179,7 +179,7 @@ namespace osu.Game.Tests.Visual.Playlists public IBindable InitialRoomsReceived { get; } = new Bindable(true); - public IBindableList Rooms => null; + public IBindableList Rooms => null!; public void AddOrUpdateRoom(Room room) => throw new NotImplementedException(); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index 2f66309f04..1636a3d4b8 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -52,11 +52,11 @@ namespace osu.Game.Tests.Visual.Playlists [SetUpSteps] public void SetupSteps() { - AddStep("set room", () => SelectedRoom.Value = new Room()); + AddStep("set room", () => SelectedRoom!.Value = new Room()); importBeatmap(); - AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(SelectedRoom.Value))); + AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(SelectedRoom!.Value))); AddUntilStep("wait for load", () => match.IsCurrentScreen()); } @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Playlists }); }); - AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom.Value.Playlist[0]); + AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom!.Value.Playlist[0]); } [Test] @@ -201,7 +201,7 @@ namespace osu.Game.Tests.Visual.Playlists private void setupAndCreateRoom(Action room) { - AddStep("setup room", () => room(SelectedRoom.Value)); + AddStep("setup room", () => room(SelectedRoom!.Value)); AddStep("click create button", () => { diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 5671cbebd7..685a685896 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -432,7 +432,7 @@ namespace osu.Game.Tests.Visual.Ranking private class RulesetWithNoPerformanceCalculator : OsuRuleset { - public override PerformanceCalculator CreatePerformanceCalculator() => null; + public override PerformanceCalculator CreatePerformanceCalculator() => null!; } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs index 9275f9755f..51da4d8755 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs @@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.UserInterface { } - public virtual IBindable UnreadCount => null; + public virtual IBindable UnreadCount { get; } = new Bindable(); public IEnumerable AllNotifications => Enumerable.Empty(); } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 1ee4670ae2..386dada328 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -567,10 +567,9 @@ namespace osu.Game.Beatmaps.Formats for (int i = pendingControlPoints.Count - 1; i >= 0; i--) { var type = pendingControlPoints[i].GetType(); - if (pendingControlPointTypes.Contains(type)) + if (!pendingControlPointTypes.Add(type)) continue; - pendingControlPointTypes.Add(type); beatmap.ControlPointInfo.Add(pendingControlPointsTime, pendingControlPoints[i]); } diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 2c500146c5..74a85cde7c 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -116,7 +116,7 @@ namespace osu.Game.Beatmaps ITrackStore IBeatmapResourceProvider.Tracks => trackStore; IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer(); AudioManager IStorageResourceProvider.AudioManager => audioManager; - RealmAccess IStorageResourceProvider.RealmAccess => null; + RealmAccess IStorageResourceProvider.RealmAccess => null!; IResourceStore IStorageResourceProvider.Files => files; IResourceStore IStorageResourceProvider.Resources => resources; IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore); diff --git a/osu.Game/Graphics/Containers/SelectionCycleFillFlowContainer.cs b/osu.Game/Graphics/Containers/SelectionCycleFillFlowContainer.cs index 62544c6111..098fd7b1ab 100644 --- a/osu.Game/Graphics/Containers/SelectionCycleFillFlowContainer.cs +++ b/osu.Game/Graphics/Containers/SelectionCycleFillFlowContainer.cs @@ -52,10 +52,10 @@ namespace osu.Game.Graphics.Containers public override void Add(T drawable) { - base.Add(drawable); - Debug.Assert(drawable != null); + base.Add(drawable); + drawable.StateChanged += state => selectionChanged(drawable, state); } diff --git a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs index af4b3849af..4af6ce7498 100644 --- a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs +++ b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Graphics.Sprites; namespace osu.Game.Graphics.UserInterface @@ -48,6 +49,7 @@ namespace osu.Game.Graphics.UserInterface { protected virtual float ChevronSize => 10; + [CanBeNull] public event Action StateChanged; public readonly SpriteIcon Chevron; diff --git a/osu.Game/IO/IStorageResourceProvider.cs b/osu.Game/IO/IStorageResourceProvider.cs index 08982a8b5f..91760971e8 100644 --- a/osu.Game/IO/IStorageResourceProvider.cs +++ b/osu.Game/IO/IStorageResourceProvider.cs @@ -41,6 +41,6 @@ namespace osu.Game.IO /// /// The underlying provider of texture data (in arbitrary image formats). /// A texture loader store. - IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore); + IResourceStore? CreateTextureLoaderStore(IResourceStore underlyingStore); } } diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 7911701853..07ee9115d6 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -264,13 +264,12 @@ namespace osu.Game.Online.Spectator { Debug.Assert(ThreadSafety.IsUpdateThread); - if (watchedUsersRefCounts.ContainsKey(userId)) + if (!watchedUsersRefCounts.TryAdd(userId, 1)) { watchedUsersRefCounts[userId]++; return; } - watchedUsersRefCounts.Add(userId, 1); WatchUserInternal(userId); } diff --git a/osu.Game/Overlays/Comments/VotePill.cs b/osu.Game/Overlays/Comments/VotePill.cs index dd418a9e58..8c5aaa062f 100644 --- a/osu.Game/Overlays/Comments/VotePill.cs +++ b/osu.Game/Overlays/Comments/VotePill.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Comments public Color4 AccentColour { get; set; } - protected override IEnumerable EffectTargets => null; + protected override IEnumerable EffectTargets => Enumerable.Empty(); [Resolved] private IAPIProvider api { get; set; } diff --git a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs index a25147b69f..f4f6fd2bc1 100644 --- a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs +++ b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using JetBrains.Annotations; using osu.Framework; using osuTK; using osu.Framework.Allocation; @@ -24,6 +25,7 @@ namespace osu.Game.Overlays.MedalSplash private const float scale_when_unlocked = 0.76f; private const float scale_when_full = 0.6f; + [CanBeNull] public event Action StateChanged; private readonly Medal medal; diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs index 9ca4c25ab9..6ec4971f06 100644 --- a/osu.Game/Overlays/Volume/VolumeMeter.cs +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -5,6 +5,7 @@ using System; using System.Globalization; +using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -48,6 +49,7 @@ namespace osu.Game.Overlays.Volume private Sample notchSample; private double sampleLastPlaybackTime; + [CanBeNull] public event Action StateChanged; private SelectionState state; diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 5662fb2293..161537200a 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -11,6 +11,7 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; @@ -139,7 +140,7 @@ namespace osu.Game.Rulesets.Objects.Drawables protected override bool RequiresChildrenUpdate => true; - public override bool IsPresent => base.IsPresent || (State.Value == ArmedState.Idle && Clock?.CurrentTime >= LifetimeStart); + public override bool IsPresent => base.IsPresent || (State.Value == ArmedState.Idle && Clock.IsNotNull() && Clock.CurrentTime >= LifetimeStart); private readonly Bindable state = new Bindable(); diff --git a/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs b/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs index fabf4fc444..7977166cb2 100644 --- a/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs +++ b/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs @@ -47,12 +47,9 @@ namespace osu.Game.Rulesets.Objects.Pooling { HitObject hitObject = entry.HitObject; - if (entryMap.ContainsKey(hitObject)) + if (!entryMap.TryAdd(hitObject, entry)) throw new InvalidOperationException($@"The {nameof(HitObjectLifetimeEntry)} is already added to this {nameof(HitObjectEntryManager)}."); - // Add the entry. - entryMap[hitObject] = entry; - // If the entry has a parent, set it and add the entry to the parent's children. if (parent != null) { diff --git a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs index 4d1f81228e..b83e565e89 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -69,6 +70,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public override void Show() => State = Visibility.Visible; + [CanBeNull] public event Action StateChanged; public partial class BoxWithBorders : CompositeDrawable diff --git a/osu.Game/Screens/Menu/ButtonArea.cs b/osu.Game/Screens/Menu/ButtonArea.cs index 69ba68442f..4eb91c526f 100644 --- a/osu.Game/Screens/Menu/ButtonArea.cs +++ b/osu.Game/Screens/Menu/ButtonArea.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using JetBrains.Annotations; using osu.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -88,6 +89,7 @@ namespace osu.Game.Screens.Menu public override void Show() => State = Visibility.Visible; + [CanBeNull] public event Action StateChanged; private partial class ButtonAreaBackground : Box, IStateful @@ -146,6 +148,7 @@ namespace osu.Game.Screens.Menu } } + [CanBeNull] public event Action StateChanged; } diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index e892f9280f..cb27d1ee61 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Development; @@ -19,6 +20,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { public partial class RoomManager : Component, IRoomManager { + [CanBeNull] public event Action RoomsUpdated; private readonly BindableList rooms = new BindableList(); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index f35b205bc4..4c0219eff5 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -509,7 +509,7 @@ namespace osu.Game.Screens.OnlinePlay.Match private void cancelTrackLooping() { - var track = Beatmap?.Value?.Track; + var track = Beatmap.Value?.Track; if (track != null) track.Looping = false; diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 1cf3d25dad..a260156595 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -109,7 +109,7 @@ namespace osu.Game.Screens.Play.HUD protected override bool OnMouseMove(MouseMoveEvent e) { - positionalAdjust = Vector2.Distance(e.MousePosition, button.ToSpaceOfOtherDrawable(button.DrawRectangle.Centre, Parent)) / 100; + positionalAdjust = Vector2.Distance(e.MousePosition, button.ToSpaceOfOtherDrawable(button.DrawRectangle.Centre, Parent!)) / 100; return base.OnMouseMove(e); } diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 93c6e72aa2..54c5b578e6 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public TestMultiplayerClient MultiplayerClient => OnlinePlayDependencies.MultiplayerClient; public new TestMultiplayerRoomManager RoomManager => OnlinePlayDependencies.RoomManager; - public TestSpectatorClient SpectatorClient => OnlinePlayDependencies?.SpectatorClient; + public TestSpectatorClient SpectatorClient => OnlinePlayDependencies.SpectatorClient; protected new MultiplayerTestSceneDependencies OnlinePlayDependencies => (MultiplayerTestSceneDependencies)base.OnlinePlayDependencies; @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("join room", () => { - SelectedRoom.Value = CreateRoom(); + SelectedRoom!.Value = CreateRoom(); RoomManager.CreateRoom(SelectedRoom.Value); }); diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs index 3509519113..eb5184353a 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs @@ -16,31 +16,31 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// /// The cached . /// - Bindable SelectedRoom { get; } + Bindable? SelectedRoom { get; } /// /// The cached /// - IRoomManager RoomManager { get; } + IRoomManager? RoomManager { get; } /// /// The cached . /// - OngoingOperationTracker OngoingOperationTracker { get; } + OngoingOperationTracker? OngoingOperationTracker { get; } /// /// The cached . /// - OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } + OnlinePlayBeatmapAvailabilityTracker? AvailabilityTracker { get; } /// /// The cached . /// - TestUserLookupCache UserLookupCache { get; } + TestUserLookupCache? UserLookupCache { get; } /// /// The cached . /// - BeatmapLookupCache BeatmapLookupCache { get; } + BeatmapLookupCache? BeatmapLookupCache { get; } } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index 87488710a7..0118d60dca 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.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 System; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -22,23 +20,23 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// public abstract partial class OnlinePlayTestScene : ScreenTestScene, IOnlinePlayTestSceneDependencies { - public Bindable SelectedRoom => OnlinePlayDependencies?.SelectedRoom; - public IRoomManager RoomManager => OnlinePlayDependencies?.RoomManager; - public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies?.OngoingOperationTracker; - public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies?.AvailabilityTracker; - public TestUserLookupCache UserLookupCache => OnlinePlayDependencies?.UserLookupCache; - public BeatmapLookupCache BeatmapLookupCache => OnlinePlayDependencies?.BeatmapLookupCache; + public Bindable SelectedRoom => OnlinePlayDependencies.SelectedRoom; + public IRoomManager RoomManager => OnlinePlayDependencies.RoomManager; + public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies.OngoingOperationTracker; + public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies.AvailabilityTracker; + public TestUserLookupCache UserLookupCache => OnlinePlayDependencies.UserLookupCache; + public BeatmapLookupCache BeatmapLookupCache => OnlinePlayDependencies.BeatmapLookupCache; /// /// All dependencies required for online play components and screens. /// - protected OnlinePlayTestSceneDependencies OnlinePlayDependencies => dependencies?.OnlinePlayDependencies; + protected OnlinePlayTestSceneDependencies OnlinePlayDependencies => dependencies.OnlinePlayDependencies; protected override Container Content => content; private readonly Container content; private readonly Container drawableDependenciesContainer; - private DelegatedDependencyContainer dependencies; + private DelegatedDependencyContainer dependencies = null!; protected OnlinePlayTestScene() { @@ -51,8 +49,10 @@ namespace osu.Game.Tests.Visual.OnlinePlay protected sealed override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { - dependencies = new DelegatedDependencyContainer(base.CreateChildDependencies(parent)); - return dependencies; + return dependencies = new DelegatedDependencyContainer(base.CreateChildDependencies(parent)) + { + OnlinePlayDependencies = initDependencies() + }; } public override void SetUpSteps() @@ -62,9 +62,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay AddStep("setup dependencies", () => { // Reset the room dependencies to a fresh state. - drawableDependenciesContainer.Clear(); - dependencies.OnlinePlayDependencies = CreateOnlinePlayDependencies(); - drawableDependenciesContainer.AddRange(OnlinePlayDependencies.DrawableComponents); + dependencies.OnlinePlayDependencies = initDependencies(); var handler = OnlinePlayDependencies.RequestsHandler; @@ -90,6 +88,14 @@ namespace osu.Game.Tests.Visual.OnlinePlay }); } + private OnlinePlayTestSceneDependencies initDependencies() + { + var newDependencies = CreateOnlinePlayDependencies(); + drawableDependenciesContainer.Clear(); + drawableDependenciesContainer.AddRange(newDependencies.DrawableComponents); + return newDependencies; + } + /// /// Creates the room dependencies. Called every . /// @@ -106,7 +112,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// /// The online play dependencies. /// - public OnlinePlayTestSceneDependencies OnlinePlayDependencies { get; set; } + public OnlinePlayTestSceneDependencies OnlinePlayDependencies { get; set; } = null!; private readonly IReadOnlyDependencyContainer parent; private readonly DependencyContainer injectableDependencies; @@ -122,10 +128,10 @@ namespace osu.Game.Tests.Visual.OnlinePlay } public object Get(Type type) - => OnlinePlayDependencies?.Get(type) ?? parent.Get(type); + => OnlinePlayDependencies.Get(type) ?? parent.Get(type); public object Get(Type type, CacheInfo info) - => OnlinePlayDependencies?.Get(type, info) ?? parent.Get(type, info); + => OnlinePlayDependencies.Get(type, info) ?? parent.Get(type, info); public void Inject(T instance) where T : class, IDependencyInjectionCandidate diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs index 975423d19b..64bd27b871 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -56,10 +56,10 @@ namespace osu.Game.Tests.Visual.OnlinePlay CacheAs(BeatmapLookupCache); } - public object Get(Type type) + public object? Get(Type type) => dependencies.Get(type); - public object Get(Type type, CacheInfo info) + public object? Get(Type type, CacheInfo info) => dependencies.Get(type, info); public void Inject(T instance) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index f371cf721f..c9acfa0ee5 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -171,10 +171,10 @@ namespace osu.Game.Tests.Visual public IRenderer Renderer => host.Renderer; public AudioManager AudioManager => Audio; - public IResourceStore Files => null; + public IResourceStore Files => null!; public new IResourceStore Resources => base.Resources; public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); - RealmAccess IStorageResourceProvider.RealmAccess => null; + RealmAccess IStorageResourceProvider.RealmAccess => null!; #endregion diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index c8c5d6745c..1bf8aa7b0b 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -66,6 +66,7 @@ HINT WARNING DO_NOT_SHOW + HINT WARNING WARNING WARNING @@ -81,6 +82,7 @@ WARNING WARNING HINT + HINT WARNING HINT DO_NOT_SHOW @@ -165,6 +167,7 @@ WARNING WARNING WARNING + HINT WARNING WARNING WARNING @@ -251,6 +254,7 @@ HINT DO_NOT_SHOW WARNING + HINT WARNING WARNING WARNING @@ -263,6 +267,7 @@ WARNING WARNING WARNING + HINT WARNING HINT HINT From 20ae88b0a0dd9a23dfbb7982d292e475f3a959c7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 2 Feb 2024 21:05:12 +0900 Subject: [PATCH 92/93] Revert unnecessary changes --- .../Tests/Visual/Multiplayer/MultiplayerTestScene.cs | 2 +- .../OnlinePlay/IOnlinePlayTestSceneDependencies.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 54c5b578e6..80c69db8b1 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("join room", () => { - SelectedRoom!.Value = CreateRoom(); + SelectedRoom.Value = CreateRoom(); RoomManager.CreateRoom(SelectedRoom.Value); }); diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs index eb5184353a..3509519113 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs @@ -16,31 +16,31 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// /// The cached . /// - Bindable? SelectedRoom { get; } + Bindable SelectedRoom { get; } /// /// The cached /// - IRoomManager? RoomManager { get; } + IRoomManager RoomManager { get; } /// /// The cached . /// - OngoingOperationTracker? OngoingOperationTracker { get; } + OngoingOperationTracker OngoingOperationTracker { get; } /// /// The cached . /// - OnlinePlayBeatmapAvailabilityTracker? AvailabilityTracker { get; } + OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } /// /// The cached . /// - TestUserLookupCache? UserLookupCache { get; } + TestUserLookupCache UserLookupCache { get; } /// /// The cached . /// - BeatmapLookupCache? BeatmapLookupCache { get; } + BeatmapLookupCache BeatmapLookupCache { get; } } } From 50f9c6102975a6fac1c2ddba4bdd1f2d203e4862 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 2 Feb 2024 22:34:48 +0900 Subject: [PATCH 93/93] Fix multiplayer tests --- .../Visual/OnlinePlay/OnlinePlayTestScene.cs | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index 0118d60dca..eebc3503bc 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// /// All dependencies required for online play components and screens. /// - protected OnlinePlayTestSceneDependencies OnlinePlayDependencies => dependencies.OnlinePlayDependencies; + protected OnlinePlayTestSceneDependencies OnlinePlayDependencies => dependencies.OnlinePlayDependencies!; protected override Container Content => content; @@ -48,12 +48,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay } protected sealed override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - return dependencies = new DelegatedDependencyContainer(base.CreateChildDependencies(parent)) - { - OnlinePlayDependencies = initDependencies() - }; - } + => dependencies = new DelegatedDependencyContainer(base.CreateChildDependencies(parent)); public override void SetUpSteps() { @@ -62,7 +57,9 @@ namespace osu.Game.Tests.Visual.OnlinePlay AddStep("setup dependencies", () => { // Reset the room dependencies to a fresh state. - dependencies.OnlinePlayDependencies = initDependencies(); + dependencies.OnlinePlayDependencies = CreateOnlinePlayDependencies(); + drawableDependenciesContainer.Clear(); + drawableDependenciesContainer.AddRange(dependencies.OnlinePlayDependencies.DrawableComponents); var handler = OnlinePlayDependencies.RequestsHandler; @@ -88,14 +85,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay }); } - private OnlinePlayTestSceneDependencies initDependencies() - { - var newDependencies = CreateOnlinePlayDependencies(); - drawableDependenciesContainer.Clear(); - drawableDependenciesContainer.AddRange(newDependencies.DrawableComponents); - return newDependencies; - } - /// /// Creates the room dependencies. Called every . /// @@ -112,7 +101,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// /// The online play dependencies. /// - public OnlinePlayTestSceneDependencies OnlinePlayDependencies { get; set; } = null!; + public OnlinePlayTestSceneDependencies? OnlinePlayDependencies { get; set; } private readonly IReadOnlyDependencyContainer parent; private readonly DependencyContainer injectableDependencies; @@ -128,10 +117,10 @@ namespace osu.Game.Tests.Visual.OnlinePlay } public object Get(Type type) - => OnlinePlayDependencies.Get(type) ?? parent.Get(type); + => OnlinePlayDependencies?.Get(type) ?? parent.Get(type); public object Get(Type type, CacheInfo info) - => OnlinePlayDependencies.Get(type, info) ?? parent.Get(type, info); + => OnlinePlayDependencies?.Get(type, info) ?? parent.Get(type, info); public void Inject(T instance) where T : class, IDependencyInjectionCandidate