From c80ecec0b418eedfa54931ef34d496984655cec1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 15:36:24 +0900 Subject: [PATCH 01/87] Reorder methods --- osu.Game/Screens/Play/Player.cs | 42 ++++++++++++++++----------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a54f9fc047..92c76ec2d2 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -531,29 +531,8 @@ namespace osu.Game.Screens.Play completionProgressDelegate = Schedule(GotoRanking); } - protected virtual ScoreInfo CreateScore() - { - var score = new ScoreInfo - { - Beatmap = Beatmap.Value.BeatmapInfo, - Ruleset = rulesetInfo, - Mods = Mods.Value.ToArray(), - }; - - if (DrawableRuleset.ReplayScore != null) - score.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser(); - else - score.User = api.LocalUser.Value; - - ScoreProcessor.PopulateScore(score); - - return score; - } - protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; - protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true); - #region Fail Logic protected FailOverlay FailOverlay { get; private set; } @@ -748,6 +727,25 @@ namespace osu.Game.Screens.Play return base.OnExiting(next); } + protected virtual ScoreInfo CreateScore() + { + var score = new ScoreInfo + { + Beatmap = Beatmap.Value.BeatmapInfo, + Ruleset = rulesetInfo, + Mods = Mods.Value.ToArray(), + }; + + if (DrawableRuleset.ReplayScore != null) + score.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser(); + else + score.User = api.LocalUser.Value; + + ScoreProcessor.PopulateScore(score); + + return score; + } + protected virtual void GotoRanking() { if (DrawableRuleset.ReplayScore != null) @@ -781,6 +779,8 @@ namespace osu.Game.Screens.Play })); } + protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true); + private void fadeOut(bool instant = false) { float fadeOutDuration = instant ? 0 : 250; From 2db7433c0b11f5a90be103621af529a2c35f50e7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 16:51:59 +0900 Subject: [PATCH 02/87] Refactor player score creation and submission process --- .../TestSceneCompletionCancellation.cs | 3 +- .../Screens/Multi/Play/TimeshiftPlayer.cs | 17 ++-- osu.Game/Screens/Play/Player.cs | 79 ++++++++++--------- osu.Game/Screens/Play/ReplayPlayer.cs | 13 +-- 4 files changed, 65 insertions(+), 47 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs index 6fd5511e5a..6e3b394057 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs @@ -10,6 +10,7 @@ using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Scoring; using osu.Game.Storyboards; using osuTK; @@ -117,7 +118,7 @@ namespace osu.Game.Tests.Visual.Gameplay { } - protected override void GotoRanking() + protected override void GotoRanking(ScoreInfo score) { GotoRankingInvoked = true; } diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index 0efa9c5196..76e4a328e0 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Logging; @@ -95,19 +96,25 @@ namespace osu.Game.Screens.Multi.Play return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem, true); } - protected override ScoreInfo CreateScore() + protected override Score CreateScore() { var score = base.CreateScore(); - score.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); + score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); + return score; + } + + protected override async Task SubmitScore(Score score) + { + await base.SubmitScore(score); Debug.Assert(token != null); - var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score); - request.Success += s => score.OnlineScoreID = s.ID; + var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score.ScoreInfo); + request.Success += s => score.ScoreInfo.OnlineScoreID = s.ID; request.Failure += e => Logger.Error(e, "Failed to submit score"); api.Queue(request); - return score; + return score.ScoreInfo; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 92c76ec2d2..3fb680b9c9 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -527,8 +528,18 @@ namespace osu.Game.Screens.Play if (!showResults) return; - using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) - completionProgressDelegate = Schedule(GotoRanking); + SubmitScore(CreateScore()).ContinueWith(t => Schedule(() => + { + using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) + { + completionProgressDelegate = Schedule(() => + { + // screen may be in the exiting transition phase. + if (this.IsCurrentScreen()) + GotoRanking(t.Result); + }); + } + })); } protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; @@ -727,60 +738,56 @@ namespace osu.Game.Screens.Play return base.OnExiting(next); } - protected virtual ScoreInfo CreateScore() + protected virtual Score CreateScore() { - var score = new ScoreInfo + var score = new Score { - Beatmap = Beatmap.Value.BeatmapInfo, - Ruleset = rulesetInfo, - Mods = Mods.Value.ToArray(), + ScoreInfo = new ScoreInfo + { + Beatmap = Beatmap.Value.BeatmapInfo, + Ruleset = rulesetInfo, + Mods = Mods.Value.ToArray(), + } }; if (DrawableRuleset.ReplayScore != null) - score.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser(); + { + score.ScoreInfo.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser(); + score.Replay = DrawableRuleset.ReplayScore.Replay; + } else - score.User = api.LocalUser.Value; + { + score.ScoreInfo.User = api.LocalUser.Value; + if (recordingScore?.Replay.Frames.Count > 0) + score.Replay = recordingScore.Replay; + } - ScoreProcessor.PopulateScore(score); + ScoreProcessor.PopulateScore(score.ScoreInfo); return score; } - protected virtual void GotoRanking() + protected virtual async Task SubmitScore(Score score) { + // Replays are already populated and present in the game's database, so should not be re-imported. if (DrawableRuleset.ReplayScore != null) + return score.ScoreInfo; + + LegacyByteArrayReader replayReader; + + using (var stream = new MemoryStream()) { - // if a replay is present, we likely don't want to import into the local database. - this.Push(CreateResults(CreateScore())); - return; + new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream); + replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); } - LegacyByteArrayReader replayReader = null; - - var score = new Score { ScoreInfo = CreateScore() }; - - if (recordingScore?.Replay.Frames.Count > 0) - { - score.Replay = recordingScore.Replay; - - using (var stream = new MemoryStream()) - { - new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream); - replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); - } - } - - scoreManager.Import(score.ScoreInfo, replayReader) - .ContinueWith(imported => Schedule(() => - { - // screen may be in the exiting transition phase. - if (this.IsCurrentScreen()) - this.Push(CreateResults(imported.Result)); - })); + return await scoreManager.Import(score.ScoreInfo, replayReader); } protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true); + protected virtual void GotoRanking(ScoreInfo score) => this.Push(CreateResults(score)); + private void fadeOut(bool instant = false) { float fadeOutDuration = instant ? 0 : 250; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 294d116f51..390d1d1959 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.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.Input.Bindings; using osu.Game.Input.Bindings; using osu.Game.Scoring; @@ -26,18 +27,20 @@ namespace osu.Game.Screens.Play DrawableRuleset?.SetReplayScore(Score); } - protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); - - protected override ScoreInfo CreateScore() + protected override Score CreateScore() { var baseScore = base.CreateScore(); // Since the replay score doesn't contain statistics, we'll pass them through here. - Score.ScoreInfo.HitEvents = baseScore.HitEvents; + Score.ScoreInfo.HitEvents = baseScore.ScoreInfo.HitEvents; - return Score.ScoreInfo; + return Score; } + protected override Task SubmitScore(Score score) => Task.FromResult(score.ScoreInfo); + + protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); + public bool OnPressed(GlobalAction action) { switch (action) From 97ff500b0daa28063188e699411b99017e2241bb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 16:56:22 +0900 Subject: [PATCH 03/87] Make timeshift wait on score submission --- .../Screens/Multi/Play/TimeshiftPlayer.cs | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index 76e4a328e0..e106dc3a1c 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -105,16 +105,29 @@ namespace osu.Game.Screens.Multi.Play protected override async Task SubmitScore(Score score) { - await base.SubmitScore(score); - Debug.Assert(token != null); + bool completed = false; var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score.ScoreInfo); - request.Success += s => score.ScoreInfo.OnlineScoreID = s.ID; - request.Failure += e => Logger.Error(e, "Failed to submit score"); + + request.Success += s => + { + score.ScoreInfo.OnlineScoreID = s.ID; + completed = true; + }; + + request.Failure += e => + { + Logger.Error(e, "Failed to submit score"); + completed = true; + }; + api.Queue(request); - return score.ScoreInfo; + while (!completed) + await Task.Delay(100); + + return await base.SubmitScore(score); } protected override void Dispose(bool isDisposing) From 2958cab239027632f0b9b5c61109bbbd90bfa8b4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 17:47:33 +0900 Subject: [PATCH 04/87] Remove GotoRanking --- .../TestSceneCompletionCancellation.cs | 13 ++++++++----- osu.Game/Screens/Play/Player.cs | 18 +++++++++++++++--- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs index 6e3b394057..4ee48fd853 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs @@ -11,6 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Scoring; +using osu.Game.Screens.Ranking; using osu.Game.Storyboards; using osuTK; @@ -51,7 +52,7 @@ namespace osu.Game.Tests.Visual.Gameplay cancel(); complete(); - AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).GotoRankingInvoked); + AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).ResultsCreated); } /// @@ -85,7 +86,7 @@ namespace osu.Game.Tests.Visual.Gameplay { // wait to ensure there was no attempt of pushing the results screen. AddWaitStep("wait", resultsDisplayWaitCount); - AddAssert("no attempt to push ranking", () => !((FakeRankingPushPlayer)Player).GotoRankingInvoked); + AddAssert("no attempt to push ranking", () => !((FakeRankingPushPlayer)Player).ResultsCreated); } protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) @@ -111,16 +112,18 @@ namespace osu.Game.Tests.Visual.Gameplay public class FakeRankingPushPlayer : TestPlayer { - public bool GotoRankingInvoked; + public bool ResultsCreated { get; private set; } public FakeRankingPushPlayer() : base(true, true) { } - protected override void GotoRanking(ScoreInfo score) + protected override ResultsScreen CreateResults(ScoreInfo score) { - GotoRankingInvoked = true; + var results = base.CreateResults(score); + ResultsCreated = true; + return results; } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 3fb680b9c9..cc5f32d300 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -536,7 +536,7 @@ namespace osu.Game.Screens.Play { // screen may be in the exiting transition phase. if (this.IsCurrentScreen()) - GotoRanking(t.Result); + this.Push(CreateResults(t.Result)); }); } })); @@ -738,6 +738,10 @@ namespace osu.Game.Screens.Play return base.OnExiting(next); } + /// + /// Creates the player's . + /// + /// The . protected virtual Score CreateScore() { var score = new Score @@ -767,6 +771,11 @@ namespace osu.Game.Screens.Play return score; } + /// + /// Submits the player's . + /// + /// The to submit. + /// The submitted score. protected virtual async Task SubmitScore(Score score) { // Replays are already populated and present in the game's database, so should not be re-imported. @@ -784,10 +793,13 @@ namespace osu.Game.Screens.Play return await scoreManager.Import(score.ScoreInfo, replayReader); } + /// + /// Creates the for a . + /// + /// The to be displayed in the results screen. + /// The . protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true); - protected virtual void GotoRanking(ScoreInfo score) => this.Push(CreateResults(score)); - private void fadeOut(bool instant = false) { float fadeOutDuration = instant ? 0 : 250; From 1369b75a86b93cac21c22c0d4a9305b9864f9258 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 17:48:42 +0900 Subject: [PATCH 05/87] Fix potential multiple submission --- osu.Game/Screens/Play/Player.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index cc5f32d300..94c595908e 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -502,6 +502,7 @@ namespace osu.Game.Screens.Play } private ScheduledDelegate completionProgressDelegate; + private Task scoreSubmissionTask; private void updateCompletionState(ValueChangedEvent completionState) { @@ -528,7 +529,8 @@ namespace osu.Game.Screens.Play if (!showResults) return; - SubmitScore(CreateScore()).ContinueWith(t => Schedule(() => + scoreSubmissionTask ??= SubmitScore(CreateScore()); + scoreSubmissionTask.ContinueWith(t => Schedule(() => { using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) { From 8826d0155908750dfcc99df83bed41e548bb3391 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 18:20:36 +0900 Subject: [PATCH 06/87] Create completion progress delegate immediately --- osu.Game/Screens/Play/Player.cs | 37 ++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 94c595908e..b79b8eeae8 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -529,21 +529,38 @@ namespace osu.Game.Screens.Play if (!showResults) return; - scoreSubmissionTask ??= SubmitScore(CreateScore()); - scoreSubmissionTask.ContinueWith(t => Schedule(() => + scoreSubmissionTask ??= Task.Run(async () => { - using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) + var score = CreateScore(); + + try { - completionProgressDelegate = Schedule(() => - { - // screen may be in the exiting transition phase. - if (this.IsCurrentScreen()) - this.Push(CreateResults(t.Result)); - }); + return await SubmitScore(score); } - })); + catch (Exception ex) + { + Logger.Error(ex, "Score submission failed!"); + return score.ScoreInfo; + } + }); + + using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) + scheduleCompletion(); } + private void scheduleCompletion() => completionProgressDelegate = Schedule(() => + { + if (!scoreSubmissionTask.IsCompleted) + { + scheduleCompletion(); + return; + } + + // screen may be in the exiting transition phase. + if (this.IsCurrentScreen()) + this.Push(CreateResults(scoreSubmissionTask.Result)); + }); + protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; #region Fail Logic From eccfc8ccd2b420c99cc2b30f868ad74e1214881a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 18:31:49 +0900 Subject: [PATCH 07/87] Fix potential cross-reference access --- osu.Game/Screens/Play/Player.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b79b8eeae8..c8f1980ab1 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -23,8 +23,10 @@ using osu.Game.Graphics.Containers; using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Overlays; +using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; @@ -781,8 +783,7 @@ namespace osu.Game.Screens.Play else { score.ScoreInfo.User = api.LocalUser.Value; - if (recordingScore?.Replay.Frames.Count > 0) - score.Replay = recordingScore.Replay; + score.Replay = new Replay { Frames = recordingScore?.Replay.Frames.ToList() ?? new List() }; } ScoreProcessor.PopulateScore(score.ScoreInfo); From 4494bb1eb5a587157f180f3ae14d078a0604194d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 00:15:41 +0900 Subject: [PATCH 08/87] Abstract RoomManager and Multiplayer --- .../Multiplayer/TestSceneMultiScreen.cs | 3 +- .../Navigation/TestSceneScreenNavigation.cs | 5 +- osu.Game/Screens/Menu/MainMenu.cs | 4 +- .../Components/ListingPollingComponent.cs | 68 ++++ .../Screens/Multi/Components/RoomManager.cs | 188 ++++++++++ .../Multi/Components/RoomPollingComponent.cs | 41 +++ .../Components/SelectionPollingComponent.cs | 69 ++++ osu.Game/Screens/Multi/IRoomManager.cs | 2 + osu.Game/Screens/Multi/Multiplayer.cs | 60 +--- osu.Game/Screens/Multi/RoomManager.cs | 337 ------------------ .../Multi/Timeshift/TimeshiftMultiplayer.cs | 48 +++ .../Multi/Timeshift/TimeshiftRoomManager.cs | 20 ++ 12 files changed, 460 insertions(+), 385 deletions(-) create mode 100644 osu.Game/Screens/Multi/Components/ListingPollingComponent.cs create mode 100644 osu.Game/Screens/Multi/Components/RoomManager.cs create mode 100644 osu.Game/Screens/Multi/Components/RoomPollingComponent.cs create mode 100644 osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs delete mode 100644 osu.Game/Screens/Multi/RoomManager.cs create mode 100644 osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs create mode 100644 osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs index 3924b0333f..0390b995e1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Overlays; +using osu.Game.Screens.Multi.Timeshift; namespace osu.Game.Tests.Visual.Multiplayer { @@ -17,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public TestSceneMultiScreen() { - Screens.Multi.Multiplayer multi = new Screens.Multi.Multiplayer(); + var multi = new TimeshiftMultiplayer(); AddStep("show", () => LoadScreen(multi)); AddUntilStep("wait for loaded", () => multi.IsLoaded); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index d87854a7ea..43f97d8ace 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -11,6 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Toolbar; +using osu.Game.Screens.Multi.Timeshift; using osu.Game.Screens.Play; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Options; @@ -107,14 +108,14 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestExitMultiWithEscape() { - PushAndConfirm(() => new Screens.Multi.Multiplayer()); + PushAndConfirm(() => new TimeshiftMultiplayer()); exitViaEscapeAndConfirm(); } [Test] public void TestExitMultiWithBackButton() { - PushAndConfirm(() => new Screens.Multi.Multiplayer()); + PushAndConfirm(() => new TimeshiftMultiplayer()); exitViaBackButtonAndConfirm(); } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index c3ecd75963..b781c347f0 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -17,7 +17,7 @@ using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; -using osu.Game.Screens.Multi; +using osu.Game.Screens.Multi.Timeshift; using osu.Game.Screens.Select; namespace osu.Game.Screens.Menu @@ -104,7 +104,7 @@ namespace osu.Game.Screens.Menu this.Push(new Editor()); }, OnSolo = onSolo, - OnMulti = delegate { this.Push(new Multiplayer()); }, + OnMulti = delegate { this.Push(new TimeshiftMultiplayer()); }, OnExit = confirmAndExit, } } diff --git a/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs b/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs new file mode 100644 index 0000000000..e22f09779e --- /dev/null +++ b/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs @@ -0,0 +1,68 @@ +// 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.Bindables; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Multi.Lounge.Components; + +namespace osu.Game.Screens.Multi.Components +{ + /// + /// A that polls for the lounge listing. + /// + public class ListingPollingComponent : RoomPollingComponent + { + [Resolved] + private Bindable currentFilter { get; set; } + + [Resolved] + private Bindable selectedRoom { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + currentFilter.BindValueChanged(_ => + { + if (IsLoaded) + PollImmediately(); + }); + } + + private GetRoomsRequest pollReq; + + protected override Task Poll() + { + if (!API.IsLoggedIn) + return base.Poll(); + + var tcs = new TaskCompletionSource(); + + pollReq?.Cancel(); + pollReq = new GetRoomsRequest(currentFilter.Value.Status, currentFilter.Value.Category); + + pollReq.Success += result => + { + for (int i = 0; i < result.Count; i++) + { + if (result[i].RoomID.Value == selectedRoom.Value?.RoomID.Value) + { + // The listing request always has less information than the opened room, so don't include it. + result[i] = selectedRoom.Value; + break; + } + } + + NotifyRoomsReceived(result); + tcs.SetResult(true); + }; + + pollReq.Failure += _ => tcs.SetResult(false); + + API.Queue(pollReq); + + return tcs.Task; + } + } +} diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs new file mode 100644 index 0000000000..46941dc58e --- /dev/null +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -0,0 +1,188 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets; + +namespace osu.Game.Screens.Multi.Components +{ + public abstract class RoomManager : CompositeDrawable, IRoomManager + { + public event Action RoomsUpdated; + + private readonly BindableList rooms = new BindableList(); + + public Bindable InitialRoomsReceived { get; } = new Bindable(); + + public IBindableList Rooms => rooms; + + [Resolved] + private RulesetStore rulesets { get; set; } + + [Resolved] + private BeatmapManager beatmaps { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } + + private Room joinedRoom; + + protected RoomManager() + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = CreatePollingComponents().Select(p => + { + p.InitialRoomsReceived.BindTo(InitialRoomsReceived); + p.RoomsReceived = onRoomsReceived; + return p; + }).ToList(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + PartRoom(); + } + + public virtual void CreateRoom(Room room, Action onSuccess = null, Action onError = null) + { + room.Host.Value = api.LocalUser.Value; + + var req = new CreateRoomRequest(room); + + req.Success += result => + { + joinedRoom = room; + + update(room, result); + addRoom(room); + + RoomsUpdated?.Invoke(); + onSuccess?.Invoke(room); + }; + + req.Failure += exception => + { + if (req.Result != null) + onError?.Invoke(req.Result.Error); + else + Logger.Log($"Failed to create the room: {exception}", level: LogLevel.Important); + }; + + api.Queue(req); + } + + private JoinRoomRequest currentJoinRoomRequest; + + public virtual void JoinRoom(Room room, Action onSuccess = null, Action onError = null) + { + currentJoinRoomRequest?.Cancel(); + currentJoinRoomRequest = new JoinRoomRequest(room); + + currentJoinRoomRequest.Success += () => + { + joinedRoom = room; + onSuccess?.Invoke(room); + }; + + currentJoinRoomRequest.Failure += exception => + { + if (!(exception is OperationCanceledException)) + Logger.Log($"Failed to join room: {exception}", level: LogLevel.Important); + onError?.Invoke(exception.ToString()); + }; + + api.Queue(currentJoinRoomRequest); + } + + public void PartRoom() + { + currentJoinRoomRequest?.Cancel(); + + if (joinedRoom == null) + return; + + api.Queue(new PartRoomRequest(joinedRoom)); + joinedRoom = null; + } + + private readonly HashSet ignoredRooms = new HashSet(); + + private void onRoomsReceived(List received) + { + // Remove past matches + foreach (var r in rooms.ToList()) + { + if (received.All(e => e.RoomID.Value != r.RoomID.Value)) + rooms.Remove(r); + } + + for (int i = 0; i < received.Count; i++) + { + var room = received[i]; + + Debug.Assert(room.RoomID.Value != null); + + if (ignoredRooms.Contains(room.RoomID.Value.Value)) + continue; + + room.Position.Value = i; + + try + { + update(room, room); + addRoom(room); + } + catch (Exception ex) + { + Logger.Error(ex, $"Failed to update room: {room.Name.Value}."); + + ignoredRooms.Add(room.RoomID.Value.Value); + rooms.Remove(room); + } + } + + RoomsUpdated?.Invoke(); + } + + /// + /// Updates a local with a remote copy. + /// + /// The local to update. + /// The remote to update with. + private void update(Room local, Room remote) + { + foreach (var pi in remote.Playlist) + pi.MapObjects(beatmaps, rulesets); + + local.CopyFrom(remote); + } + + /// + /// Adds a to the list of available rooms. + /// + /// The to add. + private void addRoom(Room room) + { + var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value); + if (existing == null) + rooms.Add(room); + else + existing.CopyFrom(room); + } + + protected abstract RoomPollingComponent[] CreatePollingComponents(); + } +} diff --git a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs new file mode 100644 index 0000000000..5430d54644 --- /dev/null +++ b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Online; +using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Screens.Multi.Components +{ + public abstract class RoomPollingComponent : PollingComponent + { + public Action> RoomsReceived; + + /// + /// The time in milliseconds to wait between polls. + /// Setting to zero stops all polling. + /// + public new readonly Bindable TimeBetweenPolls = new Bindable(); + + public IBindable InitialRoomsReceived => initialRoomsReceived; + private readonly Bindable initialRoomsReceived = new Bindable(); + + [Resolved] + protected IAPIProvider API { get; private set; } + + protected RoomPollingComponent() + { + TimeBetweenPolls.BindValueChanged(time => base.TimeBetweenPolls = time.NewValue); + } + + protected void NotifyRoomsReceived(List rooms) + { + initialRoomsReceived.Value = true; + RoomsReceived?.Invoke(rooms); + } + } +} diff --git a/osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs b/osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs new file mode 100644 index 0000000000..544d5b2388 --- /dev/null +++ b/osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs @@ -0,0 +1,69 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Screens.Multi.Components +{ + /// + /// A that polls for the currently-selected room. + /// + public class SelectionPollingComponent : RoomPollingComponent + { + [Resolved] + private Bindable selectedRoom { get; set; } + + [Resolved] + private IRoomManager roomManager { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + selectedRoom.BindValueChanged(_ => + { + if (IsLoaded) + PollImmediately(); + }); + } + + private GetRoomRequest pollReq; + + protected override Task Poll() + { + if (!API.IsLoggedIn) + return base.Poll(); + + if (selectedRoom.Value?.RoomID.Value == null) + return base.Poll(); + + var tcs = new TaskCompletionSource(); + + pollReq?.Cancel(); + pollReq = new GetRoomRequest(selectedRoom.Value.RoomID.Value.Value); + + pollReq.Success += result => + { + var rooms = new List(roomManager.Rooms); + + int index = rooms.FindIndex(r => r.RoomID == result.RoomID); + if (index < 0) + return; + + rooms[index] = result; + + NotifyRoomsReceived(rooms); + tcs.SetResult(true); + }; + + pollReq.Failure += _ => tcs.SetResult(false); + + API.Queue(pollReq); + + return tcs.Task; + } + } +} diff --git a/osu.Game/Screens/Multi/IRoomManager.cs b/osu.Game/Screens/Multi/IRoomManager.cs index bf75843c3e..3d18edcd71 100644 --- a/osu.Game/Screens/Multi/IRoomManager.cs +++ b/osu.Game/Screens/Multi/IRoomManager.cs @@ -2,11 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.Multiplayer; namespace osu.Game.Screens.Multi { + [Cached(typeof(IRoomManager))] public interface IRoomManager { /// diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index a323faeea1..5f61c5e635 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; @@ -30,7 +29,7 @@ using osuTK; namespace osu.Game.Screens.Multi { [Cached] - public class Multiplayer : OsuScreen + public abstract class Multiplayer : OsuScreen { public override bool CursorVisible => (screenStack.CurrentScreen as IMultiplayerSubScreen)?.CursorVisible ?? true; @@ -46,6 +45,9 @@ namespace osu.Game.Screens.Multi private readonly IBindable isIdle = new BindableBool(); + [Cached(Type = typeof(IRoomManager))] + protected IRoomManager RoomManager { get; private set; } + [Cached] private readonly Bindable selectedRoom = new Bindable(); @@ -55,9 +57,6 @@ namespace osu.Game.Screens.Multi [Resolved(CanBeNull = true)] private MusicController music { get; set; } - [Cached(Type = typeof(IRoomManager))] - private RoomManager roomManager; - [Resolved] private OsuGameBase game { get; set; } @@ -70,7 +69,7 @@ namespace osu.Game.Screens.Multi private readonly Drawable header; private readonly Drawable headerBackground; - public Multiplayer() + protected Multiplayer() { Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -82,7 +81,7 @@ namespace osu.Game.Screens.Multi InternalChild = waves = new MultiplayerWaveContainer { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Children = new[] { new Box { @@ -137,7 +136,7 @@ namespace osu.Game.Screens.Multi Origin = Anchor.TopRight, Action = () => CreateRoom() }, - roomManager = new RoomManager() + (Drawable)(RoomManager = CreateRoomManager()) } }; @@ -168,7 +167,7 @@ namespace osu.Game.Screens.Multi protected override void LoadComplete() { base.LoadComplete(); - isIdle.BindValueChanged(idle => updatePollingRate(idle.NewValue), true); + isIdle.BindValueChanged(idle => UpdatePollingRate(idle.NewValue), true); } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -178,36 +177,7 @@ namespace osu.Game.Screens.Multi return dependencies; } - private void updatePollingRate(bool idle) - { - if (!this.IsCurrentScreen()) - { - roomManager.TimeBetweenListingPolls = 0; - roomManager.TimeBetweenSelectionPolls = 0; - } - else - { - switch (screenStack.CurrentScreen) - { - case LoungeSubScreen _: - roomManager.TimeBetweenListingPolls = idle ? 120000 : 15000; - roomManager.TimeBetweenSelectionPolls = idle ? 120000 : 15000; - break; - - case MatchSubScreen _: - roomManager.TimeBetweenListingPolls = 0; - roomManager.TimeBetweenSelectionPolls = idle ? 30000 : 5000; - break; - - default: - roomManager.TimeBetweenListingPolls = 0; - roomManager.TimeBetweenSelectionPolls = 0; - break; - } - } - - Logger.Log($"Polling adjusted (listing: {roomManager.TimeBetweenListingPolls}, selection: {roomManager.TimeBetweenSelectionPolls})"); - } + protected abstract void UpdatePollingRate(bool isIdle); private void forcefullyExit() { @@ -241,7 +211,7 @@ namespace osu.Game.Screens.Multi beginHandlingTrack(); - updatePollingRate(isIdle.Value); + UpdatePollingRate(isIdle.Value); } public override void OnSuspending(IScreen next) @@ -251,12 +221,12 @@ namespace osu.Game.Screens.Multi endHandlingTrack(); - updatePollingRate(isIdle.Value); + UpdatePollingRate(isIdle.Value); } public override bool OnExiting(IScreen next) { - roomManager.PartRoom(); + RoomManager.PartRoom(); waves.Hide(); @@ -344,12 +314,14 @@ namespace osu.Game.Screens.Multi if (newScreen is IOsuScreen newOsuScreen) ((IBindable)Activity).BindTo(newOsuScreen.Activity); - updatePollingRate(isIdle.Value); + UpdatePollingRate(isIdle.Value); createButton.FadeTo(newScreen is LoungeSubScreen ? 1 : 0, 200); updateTrack(); } + protected IScreen CurrentSubScreen => screenStack.CurrentScreen; + private void updateTrack(ValueChangedEvent _ = null) { if (screenStack.CurrentScreen is MatchSubScreen) @@ -381,6 +353,8 @@ namespace osu.Game.Screens.Multi } } + protected abstract IRoomManager CreateRoomManager(); + private class MultiplayerWaveContainer : WaveContainer { protected override bool StartHidden => true; diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs deleted file mode 100644 index fb0cf73bb9..0000000000 --- a/osu.Game/Screens/Multi/RoomManager.cs +++ /dev/null @@ -1,337 +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.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Logging; -using osu.Game.Beatmaps; -using osu.Game.Online; -using osu.Game.Online.API; -using osu.Game.Online.Multiplayer; -using osu.Game.Rulesets; -using osu.Game.Screens.Multi.Lounge.Components; - -namespace osu.Game.Screens.Multi -{ - public class RoomManager : CompositeDrawable, IRoomManager - { - public event Action RoomsUpdated; - - private readonly BindableList rooms = new BindableList(); - - public Bindable InitialRoomsReceived { get; } = new Bindable(); - - public IBindableList Rooms => rooms; - - public double TimeBetweenListingPolls - { - get => listingPollingComponent.TimeBetweenPolls; - set => listingPollingComponent.TimeBetweenPolls = value; - } - - public double TimeBetweenSelectionPolls - { - get => selectionPollingComponent.TimeBetweenPolls; - set => selectionPollingComponent.TimeBetweenPolls = value; - } - - [Resolved] - private RulesetStore rulesets { get; set; } - - [Resolved] - private BeatmapManager beatmaps { get; set; } - - [Resolved] - private IAPIProvider api { get; set; } - - [Resolved] - private Bindable selectedRoom { get; set; } - - private readonly ListingPollingComponent listingPollingComponent; - private readonly SelectionPollingComponent selectionPollingComponent; - - private Room joinedRoom; - - public RoomManager() - { - RelativeSizeAxes = Axes.Both; - - InternalChildren = new Drawable[] - { - listingPollingComponent = new ListingPollingComponent - { - InitialRoomsReceived = { BindTarget = InitialRoomsReceived }, - RoomsReceived = onListingReceived - }, - selectionPollingComponent = new SelectionPollingComponent { RoomReceived = onSelectedRoomReceived } - }; - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - PartRoom(); - } - - public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) - { - room.Host.Value = api.LocalUser.Value; - - var req = new CreateRoomRequest(room); - - req.Success += result => - { - joinedRoom = room; - - update(room, result); - addRoom(room); - - RoomsUpdated?.Invoke(); - onSuccess?.Invoke(room); - }; - - req.Failure += exception => - { - if (req.Result != null) - onError?.Invoke(req.Result.Error); - else - Logger.Log($"Failed to create the room: {exception}", level: LogLevel.Important); - }; - - api.Queue(req); - } - - private JoinRoomRequest currentJoinRoomRequest; - - public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) - { - currentJoinRoomRequest?.Cancel(); - currentJoinRoomRequest = new JoinRoomRequest(room); - - currentJoinRoomRequest.Success += () => - { - joinedRoom = room; - onSuccess?.Invoke(room); - }; - - currentJoinRoomRequest.Failure += exception => - { - if (!(exception is OperationCanceledException)) - Logger.Log($"Failed to join room: {exception}", level: LogLevel.Important); - onError?.Invoke(exception.ToString()); - }; - - api.Queue(currentJoinRoomRequest); - } - - public void PartRoom() - { - currentJoinRoomRequest?.Cancel(); - - if (joinedRoom == null) - return; - - api.Queue(new PartRoomRequest(joinedRoom)); - joinedRoom = null; - } - - private readonly HashSet ignoredRooms = new HashSet(); - - /// - /// Invoked when the listing of all s is received from the server. - /// - /// The listing. - private void onListingReceived(List listing) - { - // Remove past matches - foreach (var r in rooms.ToList()) - { - if (listing.All(e => e.RoomID.Value != r.RoomID.Value)) - rooms.Remove(r); - } - - for (int i = 0; i < listing.Count; i++) - { - if (selectedRoom.Value?.RoomID?.Value == listing[i].RoomID.Value) - { - // The listing request contains less data than the selection request, so data from the selection request is always preferred while the room is selected. - continue; - } - - var room = listing[i]; - - Debug.Assert(room.RoomID.Value != null); - - if (ignoredRooms.Contains(room.RoomID.Value.Value)) - continue; - - room.Position.Value = i; - - try - { - update(room, room); - addRoom(room); - } - catch (Exception ex) - { - Logger.Error(ex, $"Failed to update room: {room.Name.Value}."); - - ignoredRooms.Add(room.RoomID.Value.Value); - rooms.Remove(room); - } - } - - RoomsUpdated?.Invoke(); - } - - /// - /// Invoked when a is received from the server. - /// - /// The received . - private void onSelectedRoomReceived(Room toUpdate) - { - foreach (var room in rooms) - { - if (room.RoomID.Value == toUpdate.RoomID.Value) - { - toUpdate.Position.Value = room.Position.Value; - update(room, toUpdate); - break; - } - } - } - - /// - /// Updates a local with a remote copy. - /// - /// The local to update. - /// The remote to update with. - private void update(Room local, Room remote) - { - foreach (var pi in remote.Playlist) - pi.MapObjects(beatmaps, rulesets); - - local.CopyFrom(remote); - } - - /// - /// Adds a to the list of available rooms. - /// - /// The to add. - private void addRoom(Room room) - { - var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value); - if (existing == null) - rooms.Add(room); - else - existing.CopyFrom(room); - } - - private class SelectionPollingComponent : PollingComponent - { - public Action RoomReceived; - - [Resolved] - private IAPIProvider api { get; set; } - - [Resolved] - private Bindable selectedRoom { get; set; } - - [BackgroundDependencyLoader] - private void load() - { - selectedRoom.BindValueChanged(_ => - { - if (IsLoaded) - PollImmediately(); - }); - } - - private GetRoomRequest pollReq; - - protected override Task Poll() - { - if (!api.IsLoggedIn) - return base.Poll(); - - if (selectedRoom.Value?.RoomID.Value == null) - return base.Poll(); - - var tcs = new TaskCompletionSource(); - - pollReq?.Cancel(); - pollReq = new GetRoomRequest(selectedRoom.Value.RoomID.Value.Value); - - pollReq.Success += result => - { - RoomReceived?.Invoke(result); - tcs.SetResult(true); - }; - - pollReq.Failure += _ => tcs.SetResult(false); - - api.Queue(pollReq); - - return tcs.Task; - } - } - - private class ListingPollingComponent : PollingComponent - { - public Action> RoomsReceived; - - public readonly Bindable InitialRoomsReceived = new Bindable(); - - [Resolved] - private IAPIProvider api { get; set; } - - [Resolved] - private Bindable currentFilter { get; set; } - - [BackgroundDependencyLoader] - private void load() - { - currentFilter.BindValueChanged(_ => - { - InitialRoomsReceived.Value = false; - - if (IsLoaded) - PollImmediately(); - }); - } - - private GetRoomsRequest pollReq; - - protected override Task Poll() - { - if (!api.IsLoggedIn) - return base.Poll(); - - var tcs = new TaskCompletionSource(); - - pollReq?.Cancel(); - pollReq = new GetRoomsRequest(currentFilter.Value.Status, currentFilter.Value.Category); - - pollReq.Success += result => - { - InitialRoomsReceived.Value = true; - RoomsReceived?.Invoke(result); - tcs.SetResult(true); - }; - - pollReq.Failure += _ => tcs.SetResult(false); - - api.Queue(pollReq); - - return tcs.Task; - } - } - } -} diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs new file mode 100644 index 0000000000..1ff9c670a8 --- /dev/null +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.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 osu.Framework.Logging; +using osu.Framework.Screens; +using osu.Game.Screens.Multi.Lounge; +using osu.Game.Screens.Multi.Match; + +namespace osu.Game.Screens.Multi.Timeshift +{ + public class TimeshiftMultiplayer : Multiplayer + { + protected override void UpdatePollingRate(bool isIdle) + { + var timeshiftManager = (TimeshiftRoomManager)RoomManager; + + if (!this.IsCurrentScreen()) + { + timeshiftManager.TimeBetweenListingPolls.Value = 0; + timeshiftManager.TimeBetweenSelectionPolls.Value = 0; + } + else + { + switch (CurrentSubScreen) + { + case LoungeSubScreen _: + timeshiftManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000; + timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000; + break; + + case MatchSubScreen _: + timeshiftManager.TimeBetweenListingPolls.Value = 0; + timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 30000 : 5000; + break; + + default: + timeshiftManager.TimeBetweenListingPolls.Value = 0; + timeshiftManager.TimeBetweenSelectionPolls.Value = 0; + break; + } + } + + Logger.Log($"Polling adjusted (listing: {timeshiftManager.TimeBetweenListingPolls.Value}, selection: {timeshiftManager.TimeBetweenSelectionPolls.Value})"); + } + + protected override IRoomManager CreateRoomManager() => new TimeshiftRoomManager(); + } +} diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs new file mode 100644 index 0000000000..ba96721afc --- /dev/null +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs @@ -0,0 +1,20 @@ +// 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.Bindables; +using osu.Game.Screens.Multi.Components; + +namespace osu.Game.Screens.Multi.Timeshift +{ + public class TimeshiftRoomManager : RoomManager + { + public readonly Bindable TimeBetweenListingPolls = new Bindable(); + public readonly Bindable TimeBetweenSelectionPolls = new Bindable(); + + protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] + { + new ListingPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls } }, + new SelectionPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls } } + }; + } +} From f4e9703deb20a7abdd303781a17c63e736951136 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 00:52:32 +0900 Subject: [PATCH 09/87] Fix incorrect comparison --- osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs b/osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs index 544d5b2388..37a190b5e0 100644 --- a/osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Multi.Components { var rooms = new List(roomManager.Rooms); - int index = rooms.FindIndex(r => r.RoomID == result.RoomID); + int index = rooms.FindIndex(r => r.RoomID.Value == result.RoomID.Value); if (index < 0) return; From ab9158c306c5b19c6f314967216ab4fe4915fd23 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 20:50:54 +0900 Subject: [PATCH 10/87] Add a stateful multiplayer client --- .../StatefulMultiplayerClient.cs | 389 ++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs new file mode 100644 index 0000000000..3e2f435524 --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -0,0 +1,389 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.RoomStatuses; +using osu.Game.Rulesets; +using osu.Game.Users; +using osu.Game.Utils; + +namespace osu.Game.Online.RealtimeMultiplayer +{ + public abstract class StatefulMultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer + { + /// + /// Invoked when any change occurs to the multiplayer room. + /// + public event Action? RoomChanged; + + /// + /// Invoked when the multiplayer server requests the current beatmap to be loaded into play. + /// + public event Action? LoadRequested; + + /// + /// Invoked when the multiplayer server requests gameplay to be started. + /// + public event Action? MatchStarted; + + /// + /// Invoked when the multiplayer server has finished collating results. + /// + public event Action? ResultsReady; + + /// + /// Whether the is currently connected. + /// + public abstract IBindable IsConnected { get; } + + /// + /// The joined . + /// + public MultiplayerRoom? Room { get; private set; } + + /// + /// The users currently in gameplay. + /// + public readonly BindableList PlayingUsers = new BindableList(); + + [Resolved] + private UserLookupCache userLookupCache { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + private Room? apiRoom; + private int playlistItemId; // Todo: THIS IS SUPER TEMPORARY!! + + /// + /// Joins the for a given API . + /// + /// The API . + public async Task JoinRoom(Room room) + { + Debug.Assert(Room == null); + Debug.Assert(room.RoomID.Value != null); + + apiRoom = room; + playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0; + + Room = await JoinRoom(room.RoomID.Value.Value); + + Debug.Assert(Room != null); + + foreach (var user in Room.Users) + await PopulateUser(user); + + updateLocalRoomSettings(Room.Settings); + } + + /// + /// Joins the with a given ID. + /// + /// The room ID. + /// The joined . + protected abstract Task JoinRoom(long roomId); + + public virtual Task LeaveRoom() + { + if (Room == null) + return Task.CompletedTask; + + apiRoom = null; + Room = null; + + Schedule(() => RoomChanged?.Invoke()); + + return Task.CompletedTask; + } + + /// + /// Change the current settings. + /// + /// + /// A room must have been joined via for this to have any effect. + /// + /// The new room name, if any. + /// The new room playlist item, if any. + public void ChangeSettings(Optional name = default, Optional item = default) + { + if (Room == null) + return; + + // A dummy playlist item filled with the current room settings (except mods). + var existingPlaylistItem = new PlaylistItem + { + Beatmap = + { + Value = new BeatmapInfo + { + OnlineBeatmapID = Room.Settings.BeatmapID, + MD5Hash = Room.Settings.BeatmapChecksum + } + }, + RulesetID = Room.Settings.RulesetID + }; + + var newSettings = new MultiplayerRoomSettings + { + Name = name.GetOr(Room.Settings.Name), + BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID, + BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash, + RulesetID = item.GetOr(existingPlaylistItem).RulesetID, + Mods = item.HasValue ? item.Value!.RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.Mods + }; + + // Make sure there would be a meaningful change in settings. + if (newSettings.Equals(Room.Settings)) + return; + + ChangeSettings(newSettings); + } + + public abstract Task TransferHost(int userId); + + public abstract Task ChangeSettings(MultiplayerRoomSettings settings); + + public abstract Task ChangeState(MultiplayerUserState newState); + + public abstract Task StartMatch(); + + Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state) + { + Schedule(() => + { + if (Room == null) + return; + + Debug.Assert(apiRoom != null); + + Room.State = state; + + switch (state) + { + case MultiplayerRoomState.Open: + apiRoom.Status.Value = new RoomStatusOpen(); + break; + + case MultiplayerRoomState.Playing: + apiRoom.Status.Value = new RoomStatusPlaying(); + break; + + case MultiplayerRoomState.Closed: + apiRoom.Status.Value = new RoomStatusEnded(); + break; + } + + RoomChanged?.Invoke(); + }); + + return Task.CompletedTask; + } + + async Task IMultiplayerClient.UserJoined(MultiplayerRoomUser user) + { + await PopulateUser(user); + + Schedule(() => + { + if (Room == null) + return; + + Room.Users.Add(user); + + RoomChanged?.Invoke(); + }); + } + + Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) + { + Schedule(() => + { + if (Room == null) + return; + + Room.Users.Remove(user); + PlayingUsers.Remove(user.UserID); + + RoomChanged?.Invoke(); + }); + + return Task.CompletedTask; + } + + Task IMultiplayerClient.HostChanged(int userId) + { + Schedule(() => + { + if (Room == null) + return; + + Debug.Assert(apiRoom != null); + + var user = Room.Users.FirstOrDefault(u => u.UserID == userId); + + Room.Host = user; + apiRoom.Host.Value = user?.User; + + RoomChanged?.Invoke(); + }); + + return Task.CompletedTask; + } + + Task IMultiplayerClient.SettingsChanged(MultiplayerRoomSettings newSettings) + { + updateLocalRoomSettings(newSettings); + return Task.CompletedTask; + } + + Task IMultiplayerClient.UserStateChanged(int userId, MultiplayerUserState state) + { + Schedule(() => + { + if (Room == null) + return; + + Room.Users.Single(u => u.UserID == userId).State = state; + + if (state != MultiplayerUserState.Playing) + PlayingUsers.Remove(userId); + + RoomChanged?.Invoke(); + }); + + return Task.CompletedTask; + } + + Task IMultiplayerClient.LoadRequested() + { + Schedule(() => + { + if (Room == null) + return; + + LoadRequested?.Invoke(); + }); + + return Task.CompletedTask; + } + + Task IMultiplayerClient.MatchStarted() + { + Debug.Assert(Room != null); + var players = Room.Users.Where(u => u.State == MultiplayerUserState.Playing).Select(u => u.UserID).ToList(); + + Schedule(() => + { + if (Room == null) + return; + + PlayingUsers.AddRange(players); + + MatchStarted?.Invoke(); + }); + + return Task.CompletedTask; + } + + Task IMultiplayerClient.ResultsReady() + { + Schedule(() => + { + if (Room == null) + return; + + ResultsReady?.Invoke(); + }); + + return Task.CompletedTask; + } + + /// + /// Populates the for a given . + /// + /// The to populate. + protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID); + + /// + /// Updates the local room settings with the given . + /// + /// + /// This updates both the joined and the respective API . + /// + /// The new to update from. + private void updateLocalRoomSettings(MultiplayerRoomSettings settings) + { + if (Room == null) + return; + + // Update a few instantaneously properties of the room. + Schedule(() => + { + if (Room == null) + return; + + Debug.Assert(apiRoom != null); + + Room.Settings = settings; + apiRoom.Name.Value = Room.Settings.Name; + + // The playlist update is delayed until an online beatmap lookup (below) succeeds. + // In-order for the client to not display an outdated beatmap, the playlist is forcefully cleared here. + apiRoom.Playlist.Clear(); + + RoomChanged?.Invoke(); + }); + + var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); + req.Success += res => + { + var beatmapSet = res.ToBeatmapSet(rulesets); + + var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID); + beatmap.MD5Hash = settings.BeatmapChecksum; + + var ruleset = rulesets.GetRuleset(settings.RulesetID); + var mods = settings.Mods.Select(m => m.ToMod(ruleset.CreateInstance())); + + PlaylistItem playlistItem = new PlaylistItem + { + ID = playlistItemId, + Beatmap = { Value = beatmap }, + Ruleset = { Value = ruleset }, + }; + + playlistItem.RequiredMods.AddRange(mods); + + Schedule(() => + { + if (Room == null || !Room.Settings.Equals(settings)) + return; + + Debug.Assert(apiRoom != null); + + apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity. + apiRoom.Playlist.Add(playlistItem); + }); + }; + + api.Queue(req); + } + } +} From 9ceb090f04d682f583fdc2d06fe1462f12a9c8ec Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 00:17:50 +0900 Subject: [PATCH 11/87] Fix ambiguous reference --- .../Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 3e2f435524..60960f4929 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -117,7 +117,7 @@ namespace osu.Game.Online.RealtimeMultiplayer /// Change the current settings. /// /// - /// A room must have been joined via for this to have any effect. + /// A room must be joined for this to have any effect. /// /// The new room name, if any. /// The new room playlist item, if any. From cf2340cafb8b7ce964935a410f3bd3af49041458 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 00:18:41 +0900 Subject: [PATCH 12/87] Add a realtime room manager --- .../RealtimeRoomManager.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs new file mode 100644 index 0000000000..a86e924c85 --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -0,0 +1,45 @@ +// 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.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Logging; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Screens.Multi.Components; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer +{ + public class RealtimeRoomManager : RoomManager + { + [Resolved] + private StatefulMultiplayerClient multiplayerClient { get; set; } + + public override void CreateRoom(Room room, Action onSuccess = null, Action onError = null) + => base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError); + + public override void JoinRoom(Room room, Action onSuccess = null, Action onError = null) + => base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError); + + private void joinMultiplayerRoom(Room room, Action onSuccess = null) + { + Debug.Assert(room.RoomID.Value != null); + + var joinTask = multiplayerClient.JoinRoom(room); + joinTask.ContinueWith(_ => onSuccess?.Invoke(room)); + joinTask.ContinueWith(t => + { + PartRoom(); + if (t.Exception != null) + Logger.Error(t.Exception, "Failed to join multiplayer room."); + }, TaskContinuationOptions.NotOnRanToCompletion); + } + + protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] + { + new ListingPollingComponent() + }; + } +} From a6520d3d446230cb3c0ef49aa15cdf45ba8b2232 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 01:01:09 +0900 Subject: [PATCH 13/87] Clear rooms and poll only when connected to multiplayer server --- .../Screens/Multi/Components/RoomManager.cs | 6 ++ .../RealtimeRoomManager.cs | 64 ++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index 46941dc58e..d92427680e 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -157,6 +157,12 @@ namespace osu.Game.Screens.Multi.Components RoomsUpdated?.Invoke(); } + protected void ClearRooms() + { + rooms.Clear(); + InitialRoomsReceived.Value = false; + } + /// /// Updates a local with a remote copy. /// diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index a86e924c85..62ea5d5512 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Game.Online.Multiplayer; using osu.Game.Online.RealtimeMultiplayer; @@ -17,6 +18,22 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer [Resolved] private StatefulMultiplayerClient multiplayerClient { get; set; } + public readonly Bindable TimeBetweenListingPolls = new Bindable(); + public readonly Bindable TimeBetweenSelectionPolls = new Bindable(); + private readonly IBindable isConnected = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + isConnected.BindTo(multiplayerClient.IsConnected); + isConnected.BindValueChanged(connected => Schedule(() => + { + if (!connected.NewValue) + ClearRooms(); + })); + } + public override void CreateRoom(Room room, Action onSuccess = null, Action onError = null) => base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError); @@ -39,7 +56,52 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] { - new ListingPollingComponent() + new RealtimeListingPollingComponent + { + TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls }, + AllowPolling = { BindTarget = isConnected } + }, + new RealtimeSelectionPollingComponent + { + TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls }, + AllowPolling = { BindTarget = isConnected } + } }; + + private class RealtimeListingPollingComponent : ListingPollingComponent + { + public readonly IBindable AllowPolling = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + AllowPolling.BindValueChanged(_ => + { + if (IsLoaded) + PollImmediately(); + }); + } + + protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll(); + } + + private class RealtimeSelectionPollingComponent : SelectionPollingComponent + { + public readonly IBindable AllowPolling = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + AllowPolling.BindValueChanged(_ => + { + if (IsLoaded) + PollImmediately(); + }); + } + + protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll(); + } } } From 1e2163f55e0378604ebe2f083a84b1271a8e7f4d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 01:14:50 +0900 Subject: [PATCH 14/87] Add a testable realtime multiplayer client --- .../TestRealtimeMultiplayerClient.cs | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs new file mode 100644 index 0000000000..2a90f1e744 --- /dev/null +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs @@ -0,0 +1,114 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Online.API; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.RealtimeMultiplayer +{ + public class TestRealtimeMultiplayerClient : StatefulMultiplayerClient + { + public override IBindable IsConnected { get; } = new Bindable(true); + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + public void AddUser(User user) => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(user.Id) { User = user }); + + public void RemoveUser(User user) + { + Debug.Assert(Room != null); + ((IMultiplayerClient)this).UserLeft(Room.Users.Single(u => u.User == user)); + + Schedule(() => + { + if (Room.Users.Any()) + TransferHost(Room.Users.First().UserID); + }); + } + + public void ChangeUserState(int userId, MultiplayerUserState newState) + { + Debug.Assert(Room != null); + + ((IMultiplayerClient)this).UserStateChanged(userId, newState); + + Schedule(() => + { + switch (newState) + { + case MultiplayerUserState.Loaded: + if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad)) + { + foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded)) + ChangeUserState(u.UserID, MultiplayerUserState.Playing); + + ((IMultiplayerClient)this).MatchStarted(); + } + + break; + + case MultiplayerUserState.FinishedPlay: + if (Room.Users.All(u => u.State != MultiplayerUserState.Playing)) + { + foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.FinishedPlay)) + ChangeUserState(u.UserID, MultiplayerUserState.Results); + + ((IMultiplayerClient)this).ResultsReady(); + } + + break; + } + }); + } + + protected override Task JoinRoom(long roomId) + { + var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) { User = api.LocalUser.Value }; + + var room = new MultiplayerRoom(roomId); + room.Users.Add(user); + + if (room.Users.Count == 1) + room.Host = user; + + return Task.FromResult(room); + } + + public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId); + + public override async Task ChangeSettings(MultiplayerRoomSettings settings) + { + Debug.Assert(Room != null); + + await ((IMultiplayerClient)this).SettingsChanged(settings); + + foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready)) + ChangeUserState(user.UserID, MultiplayerUserState.Idle); + } + + public override Task ChangeState(MultiplayerUserState newState) + { + ChangeUserState(api.LocalUser.Value.Id, newState); + return Task.CompletedTask; + } + + public override async Task StartMatch() + { + Debug.Assert(Room != null); + + foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready)) + ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad); + + await ((IMultiplayerClient)this).LoadRequested(); + } + } +} From 50a35c0f63222f767678d4b661b93f19e04a2b67 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 01:16:00 +0900 Subject: [PATCH 15/87] Add connection/disconnection capability --- .../RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs index 2a90f1e744..bfa8362c7e 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs @@ -16,11 +16,16 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer { public class TestRealtimeMultiplayerClient : StatefulMultiplayerClient { - public override IBindable IsConnected { get; } = new Bindable(true); + public override IBindable IsConnected => isConnected; + private readonly Bindable isConnected = new Bindable(true); [Resolved] private IAPIProvider api { get; set; } = null!; + public void Connect() => isConnected.Value = true; + + public void Disconnect() => isConnected.Value = false; + public void AddUser(User user) => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(user.Id) { User = user }); public void RemoveUser(User user) From c6555c53cc315b6df93cab9d7c56dc2c018ad2ed Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 01:17:24 +0900 Subject: [PATCH 16/87] Add a testable realtime room manager --- .../API/Requests/GetBeatmapSetRequest.cs | 10 +- .../Online/Multiplayer/CreateRoomRequest.cs | 6 +- .../TestRealtimeRoomManager.cs | 91 +++++++++++++++++++ 3 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs diff --git a/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs index 8e6deeb3c6..158ae03b8d 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs @@ -7,16 +7,16 @@ namespace osu.Game.Online.API.Requests { public class GetBeatmapSetRequest : APIRequest { - private readonly int id; - private readonly BeatmapSetLookupType type; + public readonly int ID; + public readonly BeatmapSetLookupType Type; public GetBeatmapSetRequest(int id, BeatmapSetLookupType type = BeatmapSetLookupType.SetId) { - this.id = id; - this.type = type; + ID = id; + Type = type; } - protected override string Target => type == BeatmapSetLookupType.SetId ? $@"beatmapsets/{id}" : $@"beatmapsets/lookup?beatmap_id={id}"; + protected override string Target => Type == BeatmapSetLookupType.SetId ? $@"beatmapsets/{ID}" : $@"beatmapsets/lookup?beatmap_id={ID}"; } public enum BeatmapSetLookupType diff --git a/osu.Game/Online/Multiplayer/CreateRoomRequest.cs b/osu.Game/Online/Multiplayer/CreateRoomRequest.cs index dcb4ed51ea..5be99e9442 100644 --- a/osu.Game/Online/Multiplayer/CreateRoomRequest.cs +++ b/osu.Game/Online/Multiplayer/CreateRoomRequest.cs @@ -10,11 +10,11 @@ namespace osu.Game.Online.Multiplayer { public class CreateRoomRequest : APIRequest { - private readonly Room room; + public readonly Room Room; public CreateRoomRequest(Room room) { - this.room = room; + Room = room; } protected override WebRequest CreateWebRequest() @@ -24,7 +24,7 @@ namespace osu.Game.Online.Multiplayer req.ContentType = "application/json"; req.Method = HttpMethod.Post; - req.AddRaw(JsonConvert.SerializeObject(room)); + req.AddRaw(JsonConvert.SerializeObject(Room)); return req; } diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs new file mode 100644 index 0000000000..773b72da88 --- /dev/null +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs @@ -0,0 +1,91 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Multi.RealtimeMultiplayer; + +namespace osu.Game.Tests.Visual.RealtimeMultiplayer +{ + public class TestRealtimeRoomManager : RealtimeRoomManager + { + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved] + private OsuGameBase game { get; set; } + + private readonly List rooms = new List(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + int currentScoreId = 0; + + ((DummyAPIAccess)api).HandleRequest = req => + { + switch (req) + { + case CreateRoomRequest createRoomRequest: + var createdRoom = new APICreatedRoom(); + + createdRoom.CopyFrom(createRoomRequest.Room); + createdRoom.RoomID.Value = 1; + + rooms.Add(createdRoom); + createRoomRequest.TriggerSuccess(createdRoom); + break; + + case JoinRoomRequest joinRoomRequest: + joinRoomRequest.TriggerSuccess(); + break; + + case PartRoomRequest partRoomRequest: + partRoomRequest.TriggerSuccess(); + break; + + case GetRoomsRequest getRoomsRequest: + getRoomsRequest.TriggerSuccess(rooms); + break; + + case GetBeatmapSetRequest getBeatmapSetRequest: + var onlineReq = new GetBeatmapSetRequest(getBeatmapSetRequest.ID, getBeatmapSetRequest.Type); + onlineReq.Success += res => getBeatmapSetRequest.TriggerSuccess(res); + onlineReq.Failure += e => getBeatmapSetRequest.TriggerFailure(e); + + // Get the online API from the game's dependencies. + game.Dependencies.Get().Queue(onlineReq); + break; + + case CreateRoomScoreRequest createRoomScoreRequest: + createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 }); + break; + + case SubmitRoomScoreRequest submitRoomScoreRequest: + submitRoomScoreRequest.TriggerSuccess(new MultiplayerScore + { + ID = currentScoreId++, + Accuracy = 1, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = ScoreRank.S, + MaxCombo = 1000, + TotalScore = 1000000, + User = api.LocalUser.Value, + Statistics = new Dictionary() + }); + break; + } + }; + } + + public new void Schedule(Action action) => base.Schedule(action); + } +} From c6da680c803a0d9cf055fbedbd57e7992eed823e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 01:19:08 +0900 Subject: [PATCH 17/87] Add a container for testing purposes --- .../TestRealtimeRoomContainer.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs new file mode 100644 index 0000000000..aa75968cca --- /dev/null +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.Multi.RealtimeMultiplayer; + +namespace osu.Game.Tests.Visual.RealtimeMultiplayer +{ + public class TestRealtimeRoomContainer : Container + { + protected override Container Content => content; + private readonly Container content; + + [Cached(typeof(StatefulMultiplayerClient))] + public readonly TestRealtimeMultiplayerClient Client; + + [Cached(typeof(RealtimeRoomManager))] + public readonly TestRealtimeRoomManager RoomManager; + + [Cached] + public readonly Bindable Filter = new Bindable(new FilterCriteria()); + + public TestRealtimeRoomContainer() + { + RelativeSizeAxes = Axes.Both; + + AddRangeInternal(new Drawable[] + { + Client = new TestRealtimeMultiplayerClient(), + RoomManager = new TestRealtimeRoomManager(), + content = new Container { RelativeSizeAxes = Axes.Both } + }); + } + } +} From 2fc5561b7ec7a4a5c53235203cd65b1c8d6869b8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 01:22:52 +0900 Subject: [PATCH 18/87] Add handling for GetRoomRequest() --- osu.Game/Online/Multiplayer/GetRoomRequest.cs | 6 +++--- .../TestRealtimeRoomManager.cs | 19 ++++++++++++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Multiplayer/GetRoomRequest.cs b/osu.Game/Online/Multiplayer/GetRoomRequest.cs index 2907b49f1d..449c2c8e31 100644 --- a/osu.Game/Online/Multiplayer/GetRoomRequest.cs +++ b/osu.Game/Online/Multiplayer/GetRoomRequest.cs @@ -7,13 +7,13 @@ namespace osu.Game.Online.Multiplayer { public class GetRoomRequest : APIRequest { - private readonly int roomId; + public readonly int RoomId; public GetRoomRequest(int roomId) { - this.roomId = roomId; + RoomId = roomId; } - protected override string Target => $"rooms/{roomId}"; + protected override string Target => $"rooms/{RoomId}"; } } diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs index 773b72da88..cee1c706ae 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -52,7 +53,23 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer break; case GetRoomsRequest getRoomsRequest: - getRoomsRequest.TriggerSuccess(rooms); + var roomsWithoutParticipants = new List(); + + foreach (var r in rooms) + { + var newRoom = new Room(); + + newRoom.CopyFrom(r); + newRoom.RecentParticipants.Clear(); + + roomsWithoutParticipants.Add(newRoom); + } + + getRoomsRequest.TriggerSuccess(roomsWithoutParticipants); + break; + + case GetRoomRequest getRoomRequest: + getRoomRequest.TriggerSuccess(rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId)); break; case GetBeatmapSetRequest getBeatmapSetRequest: From 9b0ca8fc3b2b380578202e5e75145d1b88ed9743 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 01:57:30 +0900 Subject: [PATCH 19/87] Make real time room manager not poll while inside a room --- .../Screens/Multi/Components/RoomManager.cs | 14 ++++++------ .../RealtimeRoomManager.cs | 22 +++++++++++++------ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index d92427680e..6e27515849 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -27,6 +27,8 @@ namespace osu.Game.Screens.Multi.Components public IBindableList Rooms => rooms; + protected Room JoinedRoom { get; private set; } + [Resolved] private RulesetStore rulesets { get; set; } @@ -36,8 +38,6 @@ namespace osu.Game.Screens.Multi.Components [Resolved] private IAPIProvider api { get; set; } - private Room joinedRoom; - protected RoomManager() { RelativeSizeAxes = Axes.Both; @@ -64,7 +64,7 @@ namespace osu.Game.Screens.Multi.Components req.Success += result => { - joinedRoom = room; + JoinedRoom = room; update(room, result); addRoom(room); @@ -93,7 +93,7 @@ namespace osu.Game.Screens.Multi.Components currentJoinRoomRequest.Success += () => { - joinedRoom = room; + JoinedRoom = room; onSuccess?.Invoke(room); }; @@ -111,11 +111,11 @@ namespace osu.Game.Screens.Multi.Components { currentJoinRoomRequest?.Cancel(); - if (joinedRoom == null) + if (JoinedRoom == null) return; - api.Queue(new PartRoomRequest(joinedRoom)); - joinedRoom = null; + api.Queue(new PartRoomRequest(JoinedRoom)); + JoinedRoom = null; } private readonly HashSet ignoredRooms = new HashSet(); diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 62ea5d5512..69addde2a6 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -21,17 +21,16 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer public readonly Bindable TimeBetweenListingPolls = new Bindable(); public readonly Bindable TimeBetweenSelectionPolls = new Bindable(); private readonly IBindable isConnected = new Bindable(); + private readonly Bindable allowPolling = new Bindable(); protected override void LoadComplete() { base.LoadComplete(); isConnected.BindTo(multiplayerClient.IsConnected); - isConnected.BindValueChanged(connected => Schedule(() => - { - if (!connected.NewValue) - ClearRooms(); - })); + isConnected.BindValueChanged(_ => Schedule(updatePolling), true); + + updatePolling(); } public override void CreateRoom(Room room, Action onSuccess = null, Action onError = null) @@ -54,17 +53,26 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer }, TaskContinuationOptions.NotOnRanToCompletion); } + private void updatePolling() + { + if (!isConnected.Value) + ClearRooms(); + + // Don't poll when not connected or when a room has been joined. + allowPolling.Value = isConnected.Value && JoinedRoom == null; + } + protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] { new RealtimeListingPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls }, - AllowPolling = { BindTarget = isConnected } + AllowPolling = { BindTarget = allowPolling } }, new RealtimeSelectionPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls }, - AllowPolling = { BindTarget = isConnected } + AllowPolling = { BindTarget = allowPolling } } }; From 7d1fe7955e633e332e2d43fafc1dd24f313e4ee5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 01:57:40 +0900 Subject: [PATCH 20/87] Small improvements to testable room manager --- .../RealtimeMultiplayer/TestRealtimeRoomManager.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs index cee1c706ae..0d1314fb51 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs @@ -5,11 +5,13 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Screens.Multi.RealtimeMultiplayer; namespace osu.Game.Tests.Visual.RealtimeMultiplayer @@ -22,6 +24,9 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer [Resolved] private OsuGameBase game { get; set; } + [Cached] + public readonly Bindable Filter = new Bindable(new FilterCriteria()); + private readonly List rooms = new List(); protected override void LoadComplete() @@ -29,6 +34,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer base.LoadComplete(); int currentScoreId = 0; + int currentRoomId = 0; ((DummyAPIAccess)api).HandleRequest = req => { @@ -38,7 +44,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer var createdRoom = new APICreatedRoom(); createdRoom.CopyFrom(createRoomRequest.Room); - createdRoom.RoomID.Value = 1; + createdRoom.RoomID.Value ??= currentRoomId++; rooms.Add(createdRoom); createRoomRequest.TriggerSuccess(createdRoom); @@ -103,6 +109,8 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer }; } + public new void ClearRooms() => base.ClearRooms(); + public new void Schedule(Action action) => base.Schedule(action); } } From 0fb8615f95b29dbff7ba1c839c3591a1b72aa5b1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:02:04 +0900 Subject: [PATCH 21/87] Implement room parting --- .../Screens/Multi/Components/RoomManager.cs | 4 +++- .../RealtimeRoomManager.cs | 20 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index 6e27515849..21bff70b8b 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -107,7 +107,7 @@ namespace osu.Game.Screens.Multi.Components api.Queue(currentJoinRoomRequest); } - public void PartRoom() + public virtual void PartRoom() { currentJoinRoomRequest?.Cancel(); @@ -157,6 +157,8 @@ namespace osu.Game.Screens.Multi.Components RoomsUpdated?.Invoke(); } + protected void RemoveRoom(Room room) => rooms.Remove(room); + protected void ClearRooms() { rooms.Clear(); diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 69addde2a6..4f73ee3865 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -23,6 +23,8 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer private readonly IBindable isConnected = new Bindable(); private readonly Bindable allowPolling = new Bindable(); + private ListingPollingComponent listingPollingComponent; + protected override void LoadComplete() { base.LoadComplete(); @@ -39,6 +41,22 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer public override void JoinRoom(Room room, Action onSuccess = null, Action onError = null) => base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError); + public override void PartRoom() + { + if (JoinedRoom == null) + return; + + var joinedRoom = JoinedRoom; + + base.PartRoom(); + multiplayerClient.LeaveRoom().Wait(); + + // Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case. + RemoveRoom(joinedRoom); + // This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling. + Schedule(() => listingPollingComponent.PollImmediately()); + } + private void joinMultiplayerRoom(Room room, Action onSuccess = null) { Debug.Assert(room.RoomID.Value != null); @@ -64,7 +82,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] { - new RealtimeListingPollingComponent + listingPollingComponent = new RealtimeListingPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls }, AllowPolling = { BindTarget = allowPolling } From a593f588db95a8ac6b8bb2594c1a94909ade9c5f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:02:47 +0900 Subject: [PATCH 22/87] Add a test for the realtime room manager --- .../TestSceneRealtimeRoomManager.cs | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs new file mode 100644 index 0000000000..6bd8c410a4 --- /dev/null +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs @@ -0,0 +1,105 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Tests.Visual.RealtimeMultiplayer +{ + public class TestSceneRealtimeRoomManager : MultiplayerTestScene + { + private TestRealtimeRoomContainer roomContainer; + private TestRealtimeRoomManager roomManager => roomContainer.RoomManager; + + [Test] + public void TestPollsInitially() + { + AddStep("create room manager with a few rooms", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + roomManager.CreateRoom(new Room { Name = { Value = "1" } }); + roomManager.PartRoom(); + roomManager.CreateRoom(new Room { Name = { Value = "2" } }); + roomManager.PartRoom(); + roomManager.ClearRooms(); + }); + }); + + AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2); + AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value); + } + + [Test] + public void TestRoomsClearedOnDisconnection() + { + AddStep("create room manager with a few rooms", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + roomManager.CreateRoom(new Room()); + roomManager.PartRoom(); + roomManager.CreateRoom(new Room()); + roomManager.PartRoom(); + }); + }); + + AddStep("disconnect", () => roomContainer.Client.Disconnect()); + + AddAssert("rooms cleared", () => roomManager.Rooms.Count == 0); + AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value); + } + + [Test] + public void TestRoomsPolledOnReconnect() + { + AddStep("create room manager with a few rooms", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + roomManager.CreateRoom(new Room()); + roomManager.PartRoom(); + roomManager.CreateRoom(new Room()); + roomManager.PartRoom(); + }); + }); + + AddStep("disconnect", () => roomContainer.Client.Disconnect()); + AddStep("connect", () => roomContainer.Client.Connect()); + + AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2); + AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value); + } + + [Test] + public void TestRoomsNotPolledWhenJoined() + { + AddStep("create room manager with a room", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + roomManager.CreateRoom(new Room()); + roomManager.ClearRooms(); + }); + }); + + AddAssert("manager not polled for rooms", () => roomManager.Rooms.Count == 0); + AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value); + } + + private TestRealtimeRoomManager createRoomManager() + { + Child = roomContainer = new TestRealtimeRoomContainer + { + RoomManager = + { + TimeBetweenListingPolls = { Value = 1 }, + TimeBetweenSelectionPolls = { Value = 1 } + } + }; + + return roomManager; + } + } +} From e84ce80d6cb0187b67527d5289ee3480e0746484 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:17:07 +0900 Subject: [PATCH 23/87] Make test headless --- .../Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs index 6bd8c410a4..9a4b748de1 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs @@ -3,10 +3,12 @@ using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Online.Multiplayer; namespace osu.Game.Tests.Visual.RealtimeMultiplayer { + [HeadlessTest] public class TestSceneRealtimeRoomManager : MultiplayerTestScene { private TestRealtimeRoomContainer roomContainer; From 109e6b4283e0a49ac13be9a59572ab1f6f599e0a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:20:02 +0900 Subject: [PATCH 24/87] Add tests for creating/joining/parting multiplayer rooms --- .../TestSceneRealtimeRoomManager.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs index 9a4b748de1..598641682b 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs @@ -90,6 +90,52 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value); } + [Test] + public void TestMultiplayerRoomJoinedWhenCreated() + { + AddStep("create room manager with a room", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + roomManager.CreateRoom(new Room()); + }); + }); + + AddAssert("multiplayer room joined", () => roomContainer.Client.Room != null); + } + + [Test] + public void TestMultiplayerRoomPartedWhenAPIRoomParted() + { + AddStep("create room manager with a room", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + roomManager.CreateRoom(new Room()); + roomManager.PartRoom(); + }); + }); + + AddAssert("multiplayer room parted", () => roomContainer.Client.Room == null); + } + + [Test] + public void TestMultiplayerRoomPartedWhenAPIRoomJoined() + { + AddStep("create room manager with a room", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + var r = new Room(); + roomManager.CreateRoom(r); + roomManager.PartRoom(); + roomManager.JoinRoom(r); + }); + }); + + AddAssert("multiplayer room parted", () => roomContainer.Client.Room != null); + } + private TestRealtimeRoomManager createRoomManager() { Child = roomContainer = new TestRealtimeRoomContainer From 3f4a66c4ae6036b98cb3512ba2440d75596c056e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:23:42 +0900 Subject: [PATCH 25/87] Add realtime multiplayer test scene abstract class --- .../RealtimeMultiplayerTestScene.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs new file mode 100644 index 0000000000..e41076a4fd --- /dev/null +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.Multi.RealtimeMultiplayer; + +namespace osu.Game.Tests.Visual.RealtimeMultiplayer +{ + public class RealtimeMultiplayerTestScene : MultiplayerTestScene + { + [Cached(typeof(StatefulMultiplayerClient))] + public TestRealtimeMultiplayerClient Client { get; } + + [Cached(typeof(RealtimeRoomManager))] + public TestRealtimeRoomManager RoomManager { get; } + + [Cached] + public Bindable Filter { get; } + + protected override Container Content => content; + private readonly TestRealtimeRoomContainer content; + + private readonly bool joinRoom; + + public RealtimeMultiplayerTestScene(bool joinRoom = true) + { + this.joinRoom = joinRoom; + base.Content.Add(content = new TestRealtimeRoomContainer { RelativeSizeAxes = Axes.Both }); + + Client = content.Client; + RoomManager = content.RoomManager; + Filter = content.Filter; + } + + [SetUp] + public new void Setup() => Schedule(() => + { + RoomManager.Schedule(() => RoomManager.PartRoom()); + + if (joinRoom) + { + Room.RoomID.Value = 1; + RoomManager.Schedule(() => RoomManager.JoinRoom(Room, null, null)); + } + }); + } +} From 9b08f573baeb360e0f5135f98f5205b7145757e5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:41:04 +0900 Subject: [PATCH 26/87] Fix room not created before being joined --- .../RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs index e41076a4fd..b52106551e 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs @@ -44,10 +44,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer RoomManager.Schedule(() => RoomManager.PartRoom()); if (joinRoom) - { - Room.RoomID.Value = 1; - RoomManager.Schedule(() => RoomManager.JoinRoom(Room, null, null)); - } + RoomManager.Schedule(() => RoomManager.CreateRoom(Room)); }); } } From 4d051818a152573834a0e859994f25c83bda1b7a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:30:53 +0900 Subject: [PATCH 27/87] Add base class for all realtime multiplayer classes --- .../RealtimeRoomComposite.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomComposite.cs diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomComposite.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomComposite.cs new file mode 100644 index 0000000000..e6d1274316 --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomComposite.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 JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Game.Online.RealtimeMultiplayer; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer +{ + public abstract class RealtimeRoomComposite : MultiplayerComposite + { + [CanBeNull] + protected MultiplayerRoom Room => Client.Room; + + [Resolved] + protected StatefulMultiplayerClient Client { get; private set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Client.RoomChanged += OnRoomChanged; + OnRoomChanged(); + } + + protected virtual void OnRoomChanged() + { + } + + protected override void Dispose(bool isDisposing) + { + if (Client != null) + Client.RoomChanged -= OnRoomChanged; + + base.Dispose(isDisposing); + } + } +} From 1e5c32410ad572883295e0f8e47e90704ef4593e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:37:47 +0900 Subject: [PATCH 28/87] Add the realtime multiplayer participants list --- .../TestSceneParticipantsList.cs | 96 +++++++++ .../Participants/ParticipantPanel.cs | 187 ++++++++++++++++++ .../Participants/ParticipantsList.cs | 55 ++++++ .../Participants/ReadyMark.cs | 51 +++++ 4 files changed, 389 insertions(+) create mode 100644 osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.cs diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs new file mode 100644 index 0000000000..ee6bbc4ecd --- /dev/null +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs @@ -0,0 +1,96 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual.RealtimeMultiplayer +{ + public class TestSceneParticipantsList : RealtimeMultiplayerTestScene + { + [SetUp] + public new void Setup() => Schedule(() => + { + Child = new ParticipantsList + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Size = new Vector2(380, 0.7f) + }; + }); + + [Test] + public void TestAddUser() + { + AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 1); + + AddStep("add user", () => Client.AddUser(new User + { + Id = 3, + Username = "Second", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + })); + + AddAssert("two unique panels", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 2); + } + + [Test] + public void TestRemoveUser() + { + User secondUser = null; + + AddStep("add a user", () => + { + Client.AddUser(secondUser = new User + { + Id = 3, + Username = "Second", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }); + }); + + AddStep("remove host", () => Client.RemoveUser(API.LocalUser.Value)); + + AddAssert("single panel is for second user", () => this.ChildrenOfType().Single().User.User == secondUser); + } + + [Test] + public void TestToggleReadyState() + { + AddAssert("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent); + + AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Ready)); + AddUntilStep("ready mark visible", () => this.ChildrenOfType().Single().IsPresent); + + AddStep("make user idle", () => Client.ChangeState(MultiplayerUserState.Idle)); + AddUntilStep("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent); + } + + [Test] + public void TestCrownChangesStateWhenHostTransferred() + { + AddStep("add user", () => Client.AddUser(new User + { + Id = 3, + Username = "Second", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + })); + + AddUntilStep("first user crown visible", () => this.ChildrenOfType().ElementAt(0).ChildrenOfType().First().Alpha == 1); + AddUntilStep("second user crown hidden", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 0); + + AddStep("make second user host", () => Client.TransferHost(3)); + + AddUntilStep("first user crown hidden", () => this.ChildrenOfType().ElementAt(0).ChildrenOfType().First().Alpha == 0); + AddUntilStep("second user crown visible", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 1); + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs new file mode 100644 index 0000000000..306a54bfdc --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs @@ -0,0 +1,187 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Users; +using osu.Game.Users.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants +{ + public class ParticipantPanel : RealtimeRoomComposite, IHasContextMenu + { + public readonly MultiplayerRoomUser User; + + [Resolved] + private IAPIProvider api { get; set; } + + private ReadyMark readyMark; + private SpriteIcon crown; + + public ParticipantPanel(MultiplayerRoomUser user) + { + User = user; + + RelativeSizeAxes = Axes.X; + Height = 40; + } + + [BackgroundDependencyLoader] + private void load() + { + Debug.Assert(User.User != null); + + var backgroundColour = Color4Extensions.FromHex("#33413C"); + + InternalChildren = new Drawable[] + { + crown = new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = FontAwesome.Solid.Crown, + Size = new Vector2(14), + Colour = Color4Extensions.FromHex("#F7E65D"), + Alpha = 0 + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 24 }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 5, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = backgroundColour + }, + new UserCoverBackground + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Width = 0.75f, + User = User.User, + Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White.Opacity(0.25f)) + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Spacing = new Vector2(10), + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new UpdateableAvatar + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + User = User.User + }, + new UpdateableFlag + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(30, 20), + Country = User.User.Country + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18), + Text = User.User.Username + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: 14), + Text = User.User.CurrentModeRank != null ? $"#{User.User.CurrentModeRank}" : string.Empty + } + } + }, + readyMark = new ReadyMark + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Margin = new MarginPadding { Right = 10 }, + Alpha = 0 + } + } + } + } + }; + } + + protected override void OnRoomChanged() + { + base.OnRoomChanged(); + + if (Room == null) + return; + + if (User.State == MultiplayerUserState.Ready) + readyMark.FadeIn(50); + else + readyMark.FadeOut(50); + + if (Room.Host?.Equals(User) == true) + crown.FadeIn(50); + else + crown.FadeOut(50); + } + + public MenuItem[] ContextMenuItems + { + get + { + if (Room == null) + return null; + + // If the local user is targetted. + if (User.UserID == api.LocalUser.Value.Id) + return null; + + // If the local user is not the host of the room. + if (Room.Host?.UserID != api.LocalUser.Value.Id) + return null; + + int targetUser = User.UserID; + + return new MenuItem[] + { + new OsuMenuItem("Give host", MenuItemType.Standard, () => + { + // Ensure the local user is still host. + if (Room.Host?.UserID != api.LocalUser.Value.Id) + return; + + Client.TransferHost(targetUser); + }) + }; + } + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs new file mode 100644 index 0000000000..d4c32d9189 --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; +using osuTK; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants +{ + public class ParticipantsList : RealtimeRoomComposite + { + private FillFlowContainer panels; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = panels = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 2) + } + } + }; + } + + protected override void OnRoomChanged() + { + base.OnRoomChanged(); + + if (Room == null) + panels.Clear(); + else + { + // Remove panels for users no longer in the room. + panels.RemoveAll(p => !Room.Users.Contains(p.User)); + + // Add panels for all users new to the room. + foreach (var user in Room.Users.Except(panels.Select(p => p.User))) + panels.Add(new ParticipantPanel(user)); + } + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.cs new file mode 100644 index 0000000000..df49d9342e --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants +{ + public class ReadyMark : CompositeDrawable + { + public ReadyMark() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12), + Text = "ready", + Colour = Color4Extensions.FromHex("#DDFFFF") + }, + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = FontAwesome.Solid.CheckCircle, + Size = new Vector2(12), + Colour = Color4Extensions.FromHex("#AADD00") + } + } + }; + } + } +} From 11a903a206a820191af25de9e4f953223869c44a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:46:16 +0900 Subject: [PATCH 29/87] Add test for many users and disable scrollbar --- .../TestSceneParticipantsList.cs | 20 +++++++++++++++++++ .../Participants/ParticipantsList.cs | 1 + 2 files changed, 21 insertions(+) diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs index ee6bbc4ecd..8c997e9e32 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs @@ -92,5 +92,25 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer AddUntilStep("first user crown hidden", () => this.ChildrenOfType().ElementAt(0).ChildrenOfType().First().Alpha == 0); AddUntilStep("second user crown visible", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 1); } + + [Test] + public void TestManyUsers() + { + AddStep("add many users", () => + { + for (int i = 0; i < 20; i++) + { + Client.AddUser(new User + { + Id = i, + Username = $"User {i}", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }); + + if (i % 2 == 0) + Client.ChangeUserState(i, MultiplayerUserState.Ready); + } + }); + } } } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs index d4c32d9189..218c2cabb7 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs @@ -24,6 +24,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants Child = new OsuScrollContainer { RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, Child = panels = new FillFlowContainer { RelativeSizeAxes = Axes.X, From e4a54dc6cdccdf17b1e950fe1abf0424e9c28485 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:52:51 +0900 Subject: [PATCH 30/87] Renamespace ready button --- osu.Game/Screens/Multi/{Match => }/Components/ReadyButton.cs | 2 +- osu.Game/Screens/Multi/Match/Components/Footer.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) rename osu.Game/Screens/Multi/{Match => }/Components/ReadyButton.cs (98%) diff --git a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs b/osu.Game/Screens/Multi/Components/ReadyButton.cs similarity index 98% rename from osu.Game/Screens/Multi/Match/Components/ReadyButton.cs rename to osu.Game/Screens/Multi/Components/ReadyButton.cs index a64f24dd7e..6d12111d8f 100644 --- a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs +++ b/osu.Game/Screens/Multi/Components/ReadyButton.cs @@ -11,7 +11,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.Multi.Components { public class ReadyButton : TriangleButton { diff --git a/osu.Game/Screens/Multi/Match/Components/Footer.cs b/osu.Game/Screens/Multi/Match/Components/Footer.cs index be4ee873fa..4ec8628d2b 100644 --- a/osu.Game/Screens/Multi/Match/Components/Footer.cs +++ b/osu.Game/Screens/Multi/Match/Components/Footer.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Multi.Components; using osuTK; namespace osu.Game.Screens.Multi.Match.Components From 4e0113afbf51e5d9be43e73b1fee714a21c04ee1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:55:48 +0900 Subject: [PATCH 31/87] Abstractify ready button and add a timeshift implementation --- .../Screens/Multi/Components/ReadyButton.cs | 24 +++--------- .../Screens/Multi/Match/Components/Footer.cs | 4 +- .../Multi/Timeshift/TimeshiftReadyButton.cs | 38 +++++++++++++++++++ 3 files changed, 46 insertions(+), 20 deletions(-) create mode 100644 osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs diff --git a/osu.Game/Screens/Multi/Components/ReadyButton.cs b/osu.Game/Screens/Multi/Components/ReadyButton.cs index 6d12111d8f..0bb4ed8617 100644 --- a/osu.Game/Screens/Multi/Components/ReadyButton.cs +++ b/osu.Game/Screens/Multi/Components/ReadyButton.cs @@ -13,26 +13,20 @@ using osu.Game.Online.Multiplayer; namespace osu.Game.Screens.Multi.Components { - public class ReadyButton : TriangleButton + public abstract class ReadyButton : TriangleButton { public readonly Bindable SelectedItem = new Bindable(); - [Resolved(typeof(Room), nameof(Room.EndDate))] - private Bindable endDate { get; set; } + public new readonly BindableBool Enabled = new BindableBool(); [Resolved] - private IBindable gameBeatmap { get; set; } + protected IBindable GameBeatmap { get; private set; } [Resolved] private BeatmapManager beatmaps { get; set; } private bool hasBeatmap; - public ReadyButton() - { - Text = "Start"; - } - private IBindable> managerUpdated; private IBindable> managerRemoved; @@ -45,10 +39,6 @@ namespace osu.Game.Screens.Multi.Components managerRemoved.BindValueChanged(beatmapRemoved); SelectedItem.BindValueChanged(item => updateSelectedItem(item.NewValue), true); - - BackgroundColour = colours.Green; - Triangles.ColourDark = colours.Green; - Triangles.ColourLight = colours.GreenLight; } private void updateSelectedItem(PlaylistItem item) @@ -94,15 +84,13 @@ namespace osu.Game.Screens.Multi.Components private void updateEnabledState() { - if (gameBeatmap.Value == null || SelectedItem.Value == null) + if (GameBeatmap.Value == null || SelectedItem.Value == null) { - Enabled.Value = false; + base.Enabled.Value = false; return; } - bool hasEnoughTime = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(gameBeatmap.Value.Track.Length) < endDate.Value; - - Enabled.Value = hasBeatmap && hasEnoughTime; + base.Enabled.Value = hasBeatmap && Enabled.Value; } } } diff --git a/osu.Game/Screens/Multi/Match/Components/Footer.cs b/osu.Game/Screens/Multi/Match/Components/Footer.cs index 4ec8628d2b..d6a7e380bf 100644 --- a/osu.Game/Screens/Multi/Match/Components/Footer.cs +++ b/osu.Game/Screens/Multi/Match/Components/Footer.cs @@ -10,7 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.Multi.Timeshift; using osuTK; namespace osu.Game.Screens.Multi.Match.Components @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Multi.Match.Components InternalChildren = new[] { background = new Box { RelativeSizeAxes = Axes.Both }, - new ReadyButton + new TimeshiftReadyButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs new file mode 100644 index 0000000000..b6698b195c --- /dev/null +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.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 System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Graphics; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Multi.Components; + +namespace osu.Game.Screens.Multi.Timeshift +{ + public class TimeshiftReadyButton : ReadyButton + { + [Resolved(typeof(Room), nameof(Room.EndDate))] + private Bindable endDate { get; set; } + + public TimeshiftReadyButton() + { + Text = "Start"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.Green; + Triangles.ColourDark = colours.Green; + Triangles.ColourLight = colours.GreenLight; + } + + protected override void Update() + { + base.Update(); + + Enabled.Value = endDate.Value == null || DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(GameBeatmap.Value.Track.Length) < endDate.Value; + } + } +} From 6efe24695b2cf930f554fd72cd9c5c51214abf6f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:59:11 +0900 Subject: [PATCH 32/87] Add the realtime multiplayer ready button --- .../TestSceneRealtimeReadyButton.cs | 129 ++++++++++++++++++ .../RealtimeReadyButton.cs | 122 +++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs new file mode 100644 index 0000000000..889c0c0be3 --- /dev/null +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs @@ -0,0 +1,129 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Graphics; +using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Rulesets; +using osu.Game.Screens.Multi.RealtimeMultiplayer; +using osu.Game.Tests.Resources; +using osu.Game.Users; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.RealtimeMultiplayer +{ + public class TestSceneRealtimeReadyButton : RealtimeMultiplayerTestScene + { + private RealtimeReadyButton button; + + private BeatmapManager beatmaps; + private RulesetStore rulesets; + + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default)); + beatmaps.Import(TestResources.GetTestBeatmapForImport(true)).Wait(); + } + + [SetUp] + public new void Setup() => Schedule(() => + { + var beatmap = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First().Beatmaps.First(); + + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); + + Child = button = new RealtimeReadyButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200, 50), + SelectedItem = + { + Value = new PlaylistItem + { + Beatmap = { Value = beatmap }, + Ruleset = { Value = beatmap.Ruleset } + } + } + }; + + Client.AddUser(API.LocalUser.Value); + }); + + [Test] + public void TestToggleStateWhenNotHost() + { + AddStep("add second user as host", () => + { + Client.AddUser(new User { Id = 2, Username = "Another user" }); + Client.TransferHost(2); + }); + + addClickButtonStep(); + AddAssert("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); + + addClickButtonStep(); + AddAssert("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); + } + + [TestCase(true)] + [TestCase(false)] + public void TestToggleStateWhenHost(bool allReady) + { + AddStep("setup", () => + { + Client.TransferHost(Client.Room?.Users[0].UserID ?? 0); + + if (!allReady) + Client.AddUser(new User { Id = 2, Username = "Another user" }); + }); + + addClickButtonStep(); + AddAssert("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); + + addClickButtonStep(); + AddAssert("match started", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); + } + + [Test] + public void TestBecomeHostWhileReady() + { + addClickButtonStep(); + AddStep("make user host", () => Client.TransferHost(Client.Room?.Users[0].UserID ?? 0)); + + addClickButtonStep(); + AddAssert("match started", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); + } + + [Test] + public void TestLoseHostWhileReady() + { + AddStep("setup", () => + { + Client.TransferHost(Client.Room?.Users[0].UserID ?? 0); + Client.AddUser(new User { Id = 2, Username = "Another user" }); + }); + + addClickButtonStep(); + AddStep("transfer host", () => Client.TransferHost(Client.Room?.Users[1].UserID ?? 0)); + + addClickButtonStep(); + AddAssert("match not started", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); + } + + private void addClickButtonStep() => AddStep("click button", () => + { + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs new file mode 100644 index 0000000000..d52df258ad --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs @@ -0,0 +1,122 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using System.Linq; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Screens.Multi.Components; +using osuTK; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer +{ + public class RealtimeReadyButton : RealtimeRoomComposite + { + public Bindable SelectedItem => button.SelectedItem; + + [Resolved] + private IAPIProvider api { get; set; } + + [CanBeNull] + private MultiplayerRoomUser localUser; + + [Resolved] + private OsuColour colours { get; set; } + + private readonly ButtonWithTrianglesExposed button; + + public RealtimeReadyButton() + { + InternalChild = button = new ButtonWithTrianglesExposed + { + RelativeSizeAxes = Axes.Both, + Size = Vector2.One, + Enabled = { Value = true }, + Action = onClick + }; + } + + protected override void OnRoomChanged() + { + base.OnRoomChanged(); + + localUser = Room?.Users.Single(u => u.User?.Id == api.LocalUser.Value.Id); + button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open; + updateState(); + } + + private void updateState() + { + if (localUser == null) + return; + + Debug.Assert(Room != null); + + switch (localUser.State) + { + case MultiplayerUserState.Idle: + button.Text = "Ready"; + updateButtonColour(true); + break; + + case MultiplayerUserState.Ready: + if (Room?.Host?.Equals(localUser) == true) + { + button.Text = "Let's go!"; + updateButtonColour(Room.Users.All(u => u.State == MultiplayerUserState.Ready)); + } + else + { + button.Text = "Waiting for host..."; + updateButtonColour(false); + } + + break; + } + } + + private void updateButtonColour(bool green) + { + if (green) + { + button.BackgroundColour = colours.Green; + button.Triangles.ColourDark = colours.Green; + button.Triangles.ColourLight = colours.GreenLight; + } + else + { + button.BackgroundColour = colours.YellowDark; + button.Triangles.ColourDark = colours.YellowDark; + button.Triangles.ColourLight = colours.Yellow; + } + } + + private void onClick() + { + if (localUser == null) + return; + + if (localUser.State == MultiplayerUserState.Idle) + Client.ChangeState(MultiplayerUserState.Ready); + else + { + if (Room?.Host?.Equals(localUser) == true) + Client.StartMatch(); + else + Client.ChangeState(MultiplayerUserState.Idle); + } + } + + private class ButtonWithTrianglesExposed : ReadyButton + { + public new Triangles Triangles => base.Triangles; + } + } +} From cc22efaa6babc4592a06a34f6affb226caea56cb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 03:17:04 +0900 Subject: [PATCH 33/87] Use tcs instead of delay-wait --- osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index e106dc3a1c..2b7ca189ee 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -107,25 +107,23 @@ namespace osu.Game.Screens.Multi.Play { Debug.Assert(token != null); - bool completed = false; + var tcs = new TaskCompletionSource(); var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score.ScoreInfo); request.Success += s => { score.ScoreInfo.OnlineScoreID = s.ID; - completed = true; + tcs.SetResult(true); }; request.Failure += e => { Logger.Error(e, "Failed to submit score"); - completed = true; + tcs.SetResult(false); }; api.Queue(request); - - while (!completed) - await Task.Delay(100); + await tcs.Task; return await base.SubmitScore(score); } From 772dd0287e4932e232d4f6c033b7246f740d3541 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 03:32:05 +0900 Subject: [PATCH 34/87] Split submission and import into two methods --- .../Screens/Multi/Play/TimeshiftPlayer.cs | 6 ++-- osu.Game/Screens/Play/Player.cs | 33 ++++++++++++++----- osu.Game/Screens/Play/ReplayPlayer.cs | 3 +- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index 2b7ca189ee..41dcf61740 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -103,8 +103,10 @@ namespace osu.Game.Screens.Multi.Play return score; } - protected override async Task SubmitScore(Score score) + protected override async Task SubmitScore(Score score) { + await base.SubmitScore(score); + Debug.Assert(token != null); var tcs = new TaskCompletionSource(); @@ -124,8 +126,6 @@ namespace osu.Game.Screens.Multi.Play api.Queue(request); await tcs.Task; - - return await base.SubmitScore(score); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index c8f1980ab1..a83f0e1b33 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -537,13 +537,23 @@ namespace osu.Game.Screens.Play try { - return await SubmitScore(score); + await SubmitScore(score); } catch (Exception ex) { Logger.Error(ex, "Score submission failed!"); - return score.ScoreInfo; } + + try + { + await ImportScore(score); + } + catch (Exception ex) + { + Logger.Error(ex, "Score import failed!"); + } + + return score.ScoreInfo; }); using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) @@ -792,15 +802,15 @@ namespace osu.Game.Screens.Play } /// - /// Submits the player's . + /// Imports the player's to the local database. /// - /// The to submit. - /// The submitted score. - protected virtual async Task SubmitScore(Score score) + /// The to import. + /// The imported score. + protected virtual async Task ImportScore(Score score) { // Replays are already populated and present in the game's database, so should not be re-imported. if (DrawableRuleset.ReplayScore != null) - return score.ScoreInfo; + return; LegacyByteArrayReader replayReader; @@ -810,9 +820,16 @@ namespace osu.Game.Screens.Play replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); } - return await scoreManager.Import(score.ScoreInfo, replayReader); + await scoreManager.Import(score.ScoreInfo, replayReader); } + /// + /// Submits the player's . + /// + /// The to submit. + /// The submitted score. + protected virtual Task SubmitScore(Score score) => Task.CompletedTask; + /// /// Creates the for a . /// diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 390d1d1959..a07213cb33 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -37,7 +37,8 @@ namespace osu.Game.Screens.Play return Score; } - protected override Task SubmitScore(Score score) => Task.FromResult(score.ScoreInfo); + // Don't re-import replay scores as they're already present in the database. + protected override Task ImportScore(Score score) => Task.CompletedTask; protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); From beaced321153867f0ae9c1d39af5a82848e2ced0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 19 Dec 2020 13:58:56 +0900 Subject: [PATCH 35/87] Remove unnecessary async state machine --- osu.Game/Screens/Play/Player.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a83f0e1b33..c539dff5d9 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -806,11 +806,11 @@ namespace osu.Game.Screens.Play /// /// The to import. /// The imported score. - protected virtual async Task ImportScore(Score score) + protected virtual Task ImportScore(Score score) { // Replays are already populated and present in the game's database, so should not be re-imported. if (DrawableRuleset.ReplayScore != null) - return; + return Task.CompletedTask; LegacyByteArrayReader replayReader; @@ -820,7 +820,7 @@ namespace osu.Game.Screens.Play replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); } - await scoreManager.Import(score.ScoreInfo, replayReader); + return scoreManager.Import(score.ScoreInfo, replayReader); } /// From d20eb368f5f86a22bf91a9f8f9838ef862effbc5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 17:36:23 +0900 Subject: [PATCH 36/87] Make return into IEnumerable --- osu.Game/Screens/Multi/Components/RoomManager.cs | 2 +- osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index 46941dc58e..ffc5e94106 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -183,6 +183,6 @@ namespace osu.Game.Screens.Multi.Components existing.CopyFrom(room); } - protected abstract RoomPollingComponent[] CreatePollingComponents(); + protected abstract IEnumerable CreatePollingComponents(); } } diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs index ba96721afc..d21f844e04 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.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.Collections.Generic; using osu.Framework.Bindables; using osu.Game.Screens.Multi.Components; @@ -11,7 +12,7 @@ namespace osu.Game.Screens.Multi.Timeshift public readonly Bindable TimeBetweenListingPolls = new Bindable(); public readonly Bindable TimeBetweenSelectionPolls = new Bindable(); - protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] + protected override IEnumerable CreatePollingComponents() => new RoomPollingComponent[] { new ListingPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls } }, new SelectionPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls } } From 7c7f15089a6ee06b3e244fc03db8e7114e165cca Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 17:42:23 +0900 Subject: [PATCH 37/87] Make CreateRoomManager return the drawable version --- osu.Game/Screens/Multi/Multiplayer.cs | 8 ++++---- osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 5f61c5e635..837ccdf2e9 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Multi private readonly IBindable isIdle = new BindableBool(); [Cached(Type = typeof(IRoomManager))] - protected IRoomManager RoomManager { get; private set; } + protected RoomManager RoomManager { get; private set; } [Cached] private readonly Bindable selectedRoom = new Bindable(); @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Multi InternalChild = waves = new MultiplayerWaveContainer { RelativeSizeAxes = Axes.Both, - Children = new[] + Children = new Drawable[] { new Box { @@ -136,7 +136,7 @@ namespace osu.Game.Screens.Multi Origin = Anchor.TopRight, Action = () => CreateRoom() }, - (Drawable)(RoomManager = CreateRoomManager()) + RoomManager = CreateRoomManager() } }; @@ -353,7 +353,7 @@ namespace osu.Game.Screens.Multi } } - protected abstract IRoomManager CreateRoomManager(); + protected abstract RoomManager CreateRoomManager(); private class MultiplayerWaveContainer : WaveContainer { diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs index 1ff9c670a8..d2d6a35a2e 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs @@ -3,6 +3,7 @@ using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Match; @@ -43,6 +44,6 @@ namespace osu.Game.Screens.Multi.Timeshift Logger.Log($"Polling adjusted (listing: {timeshiftManager.TimeBetweenListingPolls.Value}, selection: {timeshiftManager.TimeBetweenSelectionPolls.Value})"); } - protected override IRoomManager CreateRoomManager() => new TimeshiftRoomManager(); + protected override RoomManager CreateRoomManager() => new TimeshiftRoomManager(); } } From d74485704ae9c4a424bfb5fc5780b03e14f3f8be Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 17:46:45 +0900 Subject: [PATCH 38/87] Reset intial rooms received on filter change --- osu.Game/Screens/Multi/Components/ListingPollingComponent.cs | 2 ++ osu.Game/Screens/Multi/Components/RoomPollingComponent.cs | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs b/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs index e22f09779e..ebb3403950 100644 --- a/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs @@ -25,6 +25,8 @@ namespace osu.Game.Screens.Multi.Components { currentFilter.BindValueChanged(_ => { + InitialRoomsReceived.Value = false; + if (IsLoaded) PollImmediately(); }); diff --git a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs index 5430d54644..a81a9540c3 100644 --- a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs +++ b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs @@ -21,8 +21,7 @@ namespace osu.Game.Screens.Multi.Components /// public new readonly Bindable TimeBetweenPolls = new Bindable(); - public IBindable InitialRoomsReceived => initialRoomsReceived; - private readonly Bindable initialRoomsReceived = new Bindable(); + public readonly Bindable InitialRoomsReceived = new Bindable(); [Resolved] protected IAPIProvider API { get; private set; } @@ -34,7 +33,7 @@ namespace osu.Game.Screens.Multi.Components protected void NotifyRoomsReceived(List rooms) { - initialRoomsReceived.Value = true; + InitialRoomsReceived.Value = true; RoomsReceived?.Invoke(rooms); } } From 812a1d2b4f43cfb1eade5f22d9290bbe26d0fb27 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:03:30 +0900 Subject: [PATCH 39/87] Fix onSuccess callback potentially being called on failure --- .../Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 4f73ee3865..1a6e976d15 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -62,7 +62,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer Debug.Assert(room.RoomID.Value != null); var joinTask = multiplayerClient.JoinRoom(room); - joinTask.ContinueWith(_ => onSuccess?.Invoke(room)); + joinTask.ContinueWith(_ => onSuccess?.Invoke(room), TaskContinuationOptions.OnlyOnRanToCompletion); joinTask.ContinueWith(t => { PartRoom(); From fb61cdfd417701b4cacfe1aefd4bb374e344c2e5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:05:43 +0900 Subject: [PATCH 40/87] Remove unnecessary first-frame polling + address concerns --- .../Screens/Multi/Components/RoomManager.cs | 11 ++++++----- .../RealtimeMultiplayer/RealtimeRoomManager.cs | 17 ++++++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index 21bff70b8b..7b419c9efe 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -27,7 +27,8 @@ namespace osu.Game.Screens.Multi.Components public IBindableList Rooms => rooms; - protected Room JoinedRoom { get; private set; } + protected IBindable JoinedRoom => joinedRoom; + private readonly Bindable joinedRoom = new Bindable(); [Resolved] private RulesetStore rulesets { get; set; } @@ -64,7 +65,7 @@ namespace osu.Game.Screens.Multi.Components req.Success += result => { - JoinedRoom = room; + joinedRoom.Value = room; update(room, result); addRoom(room); @@ -93,7 +94,7 @@ namespace osu.Game.Screens.Multi.Components currentJoinRoomRequest.Success += () => { - JoinedRoom = room; + joinedRoom.Value = room; onSuccess?.Invoke(room); }; @@ -114,8 +115,8 @@ namespace osu.Game.Screens.Multi.Components if (JoinedRoom == null) return; - api.Queue(new PartRoomRequest(JoinedRoom)); - JoinedRoom = null; + api.Queue(new PartRoomRequest(joinedRoom.Value)); + joinedRoom.Value = null; } private readonly HashSet ignoredRooms = new HashSet(); diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 1a6e976d15..9e3c921d9e 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -30,7 +30,8 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer base.LoadComplete(); isConnected.BindTo(multiplayerClient.IsConnected); - isConnected.BindValueChanged(_ => Schedule(updatePolling), true); + isConnected.BindValueChanged(_ => Schedule(updatePolling)); + JoinedRoom.BindValueChanged(_ => updatePolling()); updatePolling(); } @@ -46,7 +47,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer if (JoinedRoom == null) return; - var joinedRoom = JoinedRoom; + var joinedRoom = JoinedRoom.Value; base.PartRoom(); multiplayerClient.LeaveRoom().Wait(); @@ -77,7 +78,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer ClearRooms(); // Don't poll when not connected or when a room has been joined. - allowPolling.Value = isConnected.Value && JoinedRoom == null; + allowPolling.Value = isConnected.Value && JoinedRoom.Value == null; } protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] @@ -102,8 +103,11 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer { base.LoadComplete(); - AllowPolling.BindValueChanged(_ => + AllowPolling.BindValueChanged(allowPolling => { + if (!allowPolling.NewValue) + return; + if (IsLoaded) PollImmediately(); }); @@ -120,8 +124,11 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer { base.LoadComplete(); - AllowPolling.BindValueChanged(_ => + AllowPolling.BindValueChanged(allowPolling => { + if (!allowPolling.NewValue) + return; + if (IsLoaded) PollImmediately(); }); From 724e4b83fe266896bef4c10f9a51840cc36a24ae Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:19:41 +0900 Subject: [PATCH 41/87] Fix nullability and remove early check --- .../StatefulMultiplayerClient.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 60960f4929..fe2f4c88f0 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Database; @@ -140,20 +141,14 @@ namespace osu.Game.Online.RealtimeMultiplayer RulesetID = Room.Settings.RulesetID }; - var newSettings = new MultiplayerRoomSettings + ChangeSettings(new MultiplayerRoomSettings { Name = name.GetOr(Room.Settings.Name), BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID, BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash, RulesetID = item.GetOr(existingPlaylistItem).RulesetID, - Mods = item.HasValue ? item.Value!.RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.Mods - }; - - // Make sure there would be a meaningful change in settings. - if (newSettings.Equals(Room.Settings)) - return; - - ChangeSettings(newSettings); + Mods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.Mods + }); } public abstract Task TransferHost(int userId); From ba4307a74c9bf82503b2e5dad391e3b28442959a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:24:13 +0900 Subject: [PATCH 42/87] Directly return task --- .../RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs index bfa8362c7e..de52633c88 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs @@ -106,14 +106,14 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer return Task.CompletedTask; } - public override async Task StartMatch() + public override Task StartMatch() { Debug.Assert(Room != null); foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready)) ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad); - await ((IMultiplayerClient)this).LoadRequested(); + return ((IMultiplayerClient)this).LoadRequested(); } } } From 1e2b425f3f0b787a747896e485a80a066b1a7429 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:24:15 +0900 Subject: [PATCH 43/87] Fix incorrect test name + assertion --- .../RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs index 598641682b..925a83a863 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer } [Test] - public void TestMultiplayerRoomPartedWhenAPIRoomJoined() + public void TestMultiplayerRoomJoinedWhenAPIRoomJoined() { AddStep("create room manager with a room", () => { @@ -133,7 +133,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer }); }); - AddAssert("multiplayer room parted", () => roomContainer.Client.Room != null); + AddAssert("multiplayer room joined", () => roomContainer.Client.Room != null); } private TestRealtimeRoomManager createRoomManager() From 8b1f5ff4927fa83700a4b22e213fe8ff0914d8d8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:25:23 +0900 Subject: [PATCH 44/87] Only instantiate ruleset once --- .../Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index fe2f4c88f0..77dbc16786 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -354,14 +354,14 @@ namespace osu.Game.Online.RealtimeMultiplayer var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID); beatmap.MD5Hash = settings.BeatmapChecksum; - var ruleset = rulesets.GetRuleset(settings.RulesetID); - var mods = settings.Mods.Select(m => m.ToMod(ruleset.CreateInstance())); + var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance(); + var mods = settings.Mods.Select(m => m.ToMod(ruleset)); PlaylistItem playlistItem = new PlaylistItem { ID = playlistItemId, Beatmap = { Value = beatmap }, - Ruleset = { Value = ruleset }, + Ruleset = { Value = ruleset.RulesetInfo }, }; playlistItem.RequiredMods.AddRange(mods); From 508f73d94961b8baae97956a3c328b41277ed434 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:25:54 +0900 Subject: [PATCH 45/87] Fix up comment --- .../Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 77dbc16786..35c8a3397e 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -328,7 +328,7 @@ namespace osu.Game.Online.RealtimeMultiplayer if (Room == null) return; - // Update a few instantaneously properties of the room. + // Update a few properties of the room instantaneously. Schedule(() => { if (Room == null) From 0cf078562dada06982d9af80136c02e145d42efa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:30:00 +0900 Subject: [PATCH 46/87] Split method up and remove nested scheduling --- .../StatefulMultiplayerClient.cs | 59 +++++++++---------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 35c8a3397e..8d3b161804 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -15,6 +15,7 @@ using osu.Game.Beatmaps; 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.Multiplayer; using osu.Game.Online.Multiplayer.RoomStatuses; using osu.Game.Rulesets; @@ -347,38 +348,36 @@ namespace osu.Game.Online.RealtimeMultiplayer }); var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); - req.Success += res => - { - var beatmapSet = res.ToBeatmapSet(rulesets); - - var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID); - beatmap.MD5Hash = settings.BeatmapChecksum; - - var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance(); - var mods = settings.Mods.Select(m => m.ToMod(ruleset)); - - PlaylistItem playlistItem = new PlaylistItem - { - ID = playlistItemId, - Beatmap = { Value = beatmap }, - Ruleset = { Value = ruleset.RulesetInfo }, - }; - - playlistItem.RequiredMods.AddRange(mods); - - Schedule(() => - { - if (Room == null || !Room.Settings.Equals(settings)) - return; - - Debug.Assert(apiRoom != null); - - apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity. - apiRoom.Playlist.Add(playlistItem); - }); - }; + req.Success += res => updatePlaylist(settings, res); api.Queue(req); } + + private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet) + { + if (Room == null || !Room.Settings.Equals(settings)) + return; + + Debug.Assert(apiRoom != null); + + var beatmapSet = onlineSet.ToBeatmapSet(rulesets); + var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID); + beatmap.MD5Hash = settings.BeatmapChecksum; + + var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance(); + var mods = settings.Mods.Select(m => m.ToMod(ruleset)); + + PlaylistItem playlistItem = new PlaylistItem + { + ID = playlistItemId, + Beatmap = { Value = beatmap }, + Ruleset = { Value = ruleset.RulesetInfo }, + }; + + playlistItem.RequiredMods.AddRange(mods); + + apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity. + apiRoom.Playlist.Add(playlistItem); + } } } From 45107280a00e310402a368a6f9b15050f1bcfd0a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:34:54 +0900 Subject: [PATCH 47/87] Make TimeBetweenPolls into a bindable --- .../Components/TestScenePollingComponent.cs | 10 +++---- osu.Game/Online/Chat/ChannelManager.cs | 2 +- osu.Game/Online/PollingComponent.cs | 30 ++++++++----------- .../Multi/Components/RoomPollingComponent.cs | 11 ------- 4 files changed, 19 insertions(+), 34 deletions(-) diff --git a/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs b/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs index fb10015ef4..2236f85b92 100644 --- a/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs +++ b/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs @@ -61,12 +61,12 @@ namespace osu.Game.Tests.Visual.Components { createPoller(true); - AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust); + AddStep("set poll interval to 1", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust); checkCount(1); checkCount(2); checkCount(3); - AddStep("set poll interval to 5", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust * 5); + AddStep("set poll interval to 5", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust * 5); checkCount(4); checkCount(4); checkCount(4); @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Components checkCount(5); checkCount(5); - AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust); + AddStep("set poll interval to 1", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust); checkCount(6); checkCount(7); } @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Components { createPoller(false); - AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust * 5); + AddStep("set poll interval to 1", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust * 5); checkCount(0); skip(); checkCount(0); @@ -141,7 +141,7 @@ namespace osu.Game.Tests.Visual.Components public class TestSlowPoller : TestPoller { - protected override Task Poll() => Task.Delay((int)(TimeBetweenPolls / 2f / Clock.Rate)).ContinueWith(_ => base.Poll()); + protected override Task Poll() => Task.Delay((int)(TimeBetweenPolls.Value / 2f / Clock.Rate)).ContinueWith(_ => base.Poll()); } } } diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 16f46581c5..62ae507419 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -57,7 +57,7 @@ namespace osu.Game.Online.Chat { CurrentChannel.ValueChanged += currentChannelChanged; - HighPollRate.BindValueChanged(enabled => TimeBetweenPolls = enabled.NewValue ? 1000 : 6000, true); + HighPollRate.BindValueChanged(enabled => TimeBetweenPolls.Value = enabled.NewValue ? 1000 : 6000, true); } /// diff --git a/osu.Game/Online/PollingComponent.cs b/osu.Game/Online/PollingComponent.cs index 228f147835..3d19f2ab09 100644 --- a/osu.Game/Online/PollingComponent.cs +++ b/osu.Game/Online/PollingComponent.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; +using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Framework.Threading; @@ -19,22 +20,11 @@ namespace osu.Game.Online private bool pollingActive; - private double timeBetweenPolls; - /// /// The time in milliseconds to wait between polls. /// Setting to zero stops all polling. /// - public double TimeBetweenPolls - { - get => timeBetweenPolls; - set - { - timeBetweenPolls = value; - scheduledPoll?.Cancel(); - pollIfNecessary(); - } - } + public readonly Bindable TimeBetweenPolls = new Bindable(); /// /// @@ -42,7 +32,13 @@ namespace osu.Game.Online /// The initial time in milliseconds to wait between polls. Setting to zero stops all polling. protected PollingComponent(double timeBetweenPolls = 0) { - TimeBetweenPolls = timeBetweenPolls; + TimeBetweenPolls.BindValueChanged(_ => + { + scheduledPoll?.Cancel(); + pollIfNecessary(); + }); + + TimeBetweenPolls.Value = timeBetweenPolls; } protected override void LoadComplete() @@ -60,7 +56,7 @@ namespace osu.Game.Online if (pollingActive) return false; // don't try polling if the time between polls hasn't been set. - if (timeBetweenPolls == 0) return false; + if (TimeBetweenPolls.Value == 0) return false; if (!lastTimePolled.HasValue) { @@ -68,7 +64,7 @@ namespace osu.Game.Online return true; } - if (Time.Current - lastTimePolled.Value > timeBetweenPolls) + if (Time.Current - lastTimePolled.Value > TimeBetweenPolls.Value) { doPoll(); return true; @@ -99,7 +95,7 @@ namespace osu.Game.Online /// public void PollImmediately() { - lastTimePolled = Time.Current - timeBetweenPolls; + lastTimePolled = Time.Current - TimeBetweenPolls.Value; scheduleNextPoll(); } @@ -121,7 +117,7 @@ namespace osu.Game.Online double lastPollDuration = lastTimePolled.HasValue ? Time.Current - lastTimePolled.Value : 0; - scheduledPoll = Scheduler.AddDelayed(doPoll, Math.Max(0, timeBetweenPolls - lastPollDuration)); + scheduledPoll = Scheduler.AddDelayed(doPoll, Math.Max(0, TimeBetweenPolls.Value - lastPollDuration)); } } } diff --git a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs index a81a9540c3..ad0720db7b 100644 --- a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs +++ b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs @@ -15,22 +15,11 @@ namespace osu.Game.Screens.Multi.Components { public Action> RoomsReceived; - /// - /// The time in milliseconds to wait between polls. - /// Setting to zero stops all polling. - /// - public new readonly Bindable TimeBetweenPolls = new Bindable(); - public readonly Bindable InitialRoomsReceived = new Bindable(); [Resolved] protected IAPIProvider API { get; private set; } - protected RoomPollingComponent() - { - TimeBetweenPolls.BindValueChanged(time => base.TimeBetweenPolls = time.NewValue); - } - protected void NotifyRoomsReceived(List rooms) { InitialRoomsReceived.Value = true; From ce2560b545deea1076c5f6cfd781e9089a360061 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:36:31 +0900 Subject: [PATCH 48/87] Extract value into const --- .../Participants/ParticipantPanel.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs index 306a54bfdc..002849a275 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs @@ -142,15 +142,17 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants if (Room == null) return; + const double fade_time = 50; + if (User.State == MultiplayerUserState.Ready) - readyMark.FadeIn(50); + readyMark.FadeIn(fade_time); else - readyMark.FadeOut(50); + readyMark.FadeOut(fade_time); if (Room.Host?.Equals(User) == true) - crown.FadeIn(50); + crown.FadeIn(fade_time); else - crown.FadeOut(50); + crown.FadeOut(fade_time); } public MenuItem[] ContextMenuItems From 19db35501e156ceaaf45f65bb4360dc4d859a68f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:44:36 +0900 Subject: [PATCH 49/87] Fix incorrect end date usage in timeshift ready button --- osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs index b6698b195c..ba639c29f4 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.Multi.Timeshift public class TimeshiftReadyButton : ReadyButton { [Resolved(typeof(Room), nameof(Room.EndDate))] - private Bindable endDate { get; set; } + private Bindable endDate { get; set; } public TimeshiftReadyButton() { @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Multi.Timeshift { base.Update(); - Enabled.Value = endDate.Value == null || DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(GameBeatmap.Value.Track.Length) < endDate.Value; + Enabled.Value = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(GameBeatmap.Value.Track.Length) < endDate.Value; } } } From a07a36793a5d90781178b0f94743580359c17973 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:44:41 +0900 Subject: [PATCH 50/87] Fix test not working --- .../RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs index 889c0c0be3..1f863028af 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs @@ -97,6 +97,12 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer [Test] public void TestBecomeHostWhileReady() { + AddStep("add host", () => + { + Client.AddUser(new User { Id = 2, Username = "Another user" }); + Client.TransferHost(2); + }); + addClickButtonStep(); AddStep("make user host", () => Client.TransferHost(Client.Room?.Users[0].UserID ?? 0)); From b002c466660535e32e036d322e97e68335c6c497 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:49:39 +0900 Subject: [PATCH 51/87] Add number of ready users to button --- .../Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs index d52df258ad..ea8fb04994 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs @@ -69,8 +69,9 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer case MultiplayerUserState.Ready: if (Room?.Host?.Equals(localUser) == true) { - button.Text = "Let's go!"; - updateButtonColour(Room.Users.All(u => u.State == MultiplayerUserState.Ready)); + int countReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready); + button.Text = $"Start match ({countReady} / {Room.Users.Count} ready)"; + updateButtonColour(true); } else { From f876a329b1b4e8b2cbef4effd6ef242e1be1c233 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 22:51:33 +0900 Subject: [PATCH 52/87] Fire-and-forget leave-room request --- .../StatefulMultiplayerClient.cs | 29 +++++++++++++++++-- .../RealtimeRoomManager.cs | 2 +- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 60960f4929..b846d6732f 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -77,7 +77,9 @@ namespace osu.Game.Online.RealtimeMultiplayer /// The API . public async Task JoinRoom(Room room) { - Debug.Assert(Room == null); + if (Room != null) + throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); + Debug.Assert(room.RoomID.Value != null); apiRoom = room; @@ -166,6 +168,9 @@ namespace osu.Game.Online.RealtimeMultiplayer Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state) { + if (Room == null) + return Task.CompletedTask; + Schedule(() => { if (Room == null) @@ -198,6 +203,9 @@ namespace osu.Game.Online.RealtimeMultiplayer async Task IMultiplayerClient.UserJoined(MultiplayerRoomUser user) { + if (Room == null) + return; + await PopulateUser(user); Schedule(() => @@ -213,6 +221,9 @@ namespace osu.Game.Online.RealtimeMultiplayer Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) { + if (Room == null) + return Task.CompletedTask; + Schedule(() => { if (Room == null) @@ -229,6 +240,9 @@ namespace osu.Game.Online.RealtimeMultiplayer Task IMultiplayerClient.HostChanged(int userId) { + if (Room == null) + return Task.CompletedTask; + Schedule(() => { if (Room == null) @@ -255,6 +269,9 @@ namespace osu.Game.Online.RealtimeMultiplayer Task IMultiplayerClient.UserStateChanged(int userId, MultiplayerUserState state) { + if (Room == null) + return Task.CompletedTask; + Schedule(() => { if (Room == null) @@ -273,6 +290,9 @@ namespace osu.Game.Online.RealtimeMultiplayer Task IMultiplayerClient.LoadRequested() { + if (Room == null) + return Task.CompletedTask; + Schedule(() => { if (Room == null) @@ -286,7 +306,9 @@ namespace osu.Game.Online.RealtimeMultiplayer Task IMultiplayerClient.MatchStarted() { - Debug.Assert(Room != null); + if (Room == null) + return Task.CompletedTask; + var players = Room.Users.Where(u => u.State == MultiplayerUserState.Playing).Select(u => u.UserID).ToList(); Schedule(() => @@ -304,6 +326,9 @@ namespace osu.Game.Online.RealtimeMultiplayer Task IMultiplayerClient.ResultsReady() { + if (Room == null) + return Task.CompletedTask; + Schedule(() => { if (Room == null) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 4f73ee3865..d2a03da714 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer var joinedRoom = JoinedRoom; base.PartRoom(); - multiplayerClient.LeaveRoom().Wait(); + multiplayerClient.LeaveRoom(); // Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case. RemoveRoom(joinedRoom); From 9d13a5b06a9f90e9cd9a8e489594f03b3d3ec4f0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 22:53:07 +0900 Subject: [PATCH 53/87] Fix potential cross-thread list access --- .../Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index b846d6732f..bf58849e35 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -309,14 +309,12 @@ namespace osu.Game.Online.RealtimeMultiplayer if (Room == null) return Task.CompletedTask; - var players = Room.Users.Where(u => u.State == MultiplayerUserState.Playing).Select(u => u.UserID).ToList(); - Schedule(() => { if (Room == null) return; - PlayingUsers.AddRange(players); + PlayingUsers.AddRange(Room.Users.Where(u => u.State == MultiplayerUserState.Playing).Select(u => u.UserID)); MatchStarted?.Invoke(); }); From c33e693b8e3331af505931950940dc7684b04f41 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 23:05:17 +0900 Subject: [PATCH 54/87] Refactor InitialRoomsReceived to avoid extra bindables --- .../Visual/Multiplayer/TestRoomManager.cs | 2 +- .../Multiplayer/TestSceneMatchSettingsOverlay.cs | 2 +- .../Multiplayer/TestSceneMatchSubScreen.cs | 2 +- .../Multi/Components/ListingPollingComponent.cs | 3 +-- osu.Game/Screens/Multi/Components/RoomManager.cs | 12 ++++++++++-- .../Multi/Components/RoomPollingComponent.cs | 16 ++++++++-------- osu.Game/Screens/Multi/IRoomManager.cs | 2 +- osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs | 2 +- 8 files changed, 24 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs index 67a53307fc..9dd4aea4bd 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public readonly BindableList Rooms = new BindableList(); - public Bindable InitialRoomsReceived { get; } = new Bindable(true); + public IBindable InitialRoomsReceived { get; } = new Bindable(true); IBindableList IRoomManager.Rooms => Rooms; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs index cbe8cc6137..234374ee2b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs @@ -131,7 +131,7 @@ namespace osu.Game.Tests.Visual.Multiplayer remove { } } - public Bindable InitialRoomsReceived { get; } = new Bindable(true); + public IBindable InitialRoomsReceived { get; } = new Bindable(true); public IBindableList Rooms => null; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs index 65e9893851..bceb6efac1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs @@ -151,7 +151,7 @@ namespace osu.Game.Tests.Visual.Multiplayer remove => throw new NotImplementedException(); } - public Bindable InitialRoomsReceived { get; } = new Bindable(true); + public IBindable InitialRoomsReceived { get; } = new Bindable(true); public IBindableList Rooms { get; } = new BindableList(); diff --git a/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs b/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs index ebb3403950..dff6c50bf2 100644 --- a/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs @@ -25,8 +25,7 @@ namespace osu.Game.Screens.Multi.Components { currentFilter.BindValueChanged(_ => { - InitialRoomsReceived.Value = false; - + NotifyRoomsReceived(null); if (IsLoaded) PollImmediately(); }); diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index ffc5e94106..382ce52723 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -23,7 +23,8 @@ namespace osu.Game.Screens.Multi.Components private readonly BindableList rooms = new BindableList(); - public Bindable InitialRoomsReceived { get; } = new Bindable(); + public IBindable InitialRoomsReceived => initialRoomsReceived; + private readonly Bindable initialRoomsReceived = new Bindable(); public IBindableList Rooms => rooms; @@ -44,7 +45,6 @@ namespace osu.Game.Screens.Multi.Components InternalChildren = CreatePollingComponents().Select(p => { - p.InitialRoomsReceived.BindTo(InitialRoomsReceived); p.RoomsReceived = onRoomsReceived; return p; }).ToList(); @@ -122,6 +122,13 @@ namespace osu.Game.Screens.Multi.Components private void onRoomsReceived(List received) { + if (received == null) + { + rooms.Clear(); + initialRoomsReceived.Value = false; + return; + } + // Remove past matches foreach (var r in rooms.ToList()) { @@ -155,6 +162,7 @@ namespace osu.Game.Screens.Multi.Components } RoomsUpdated?.Invoke(); + initialRoomsReceived.Value = true; } /// diff --git a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs index ad0720db7b..fbaf9dd930 100644 --- a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs +++ b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; @@ -13,17 +12,18 @@ namespace osu.Game.Screens.Multi.Components { public abstract class RoomPollingComponent : PollingComponent { + /// + /// Invoked when any s have been received from the API. + /// + /// Any s present locally but not returned by this event are to be removed from display. + /// If null, the display of local rooms is reset to an initial state. + /// + /// public Action> RoomsReceived; - public readonly Bindable InitialRoomsReceived = new Bindable(); - [Resolved] protected IAPIProvider API { get; private set; } - protected void NotifyRoomsReceived(List rooms) - { - InitialRoomsReceived.Value = true; - RoomsReceived?.Invoke(rooms); - } + protected void NotifyRoomsReceived(List rooms) => RoomsReceived?.Invoke(rooms); } } diff --git a/osu.Game/Screens/Multi/IRoomManager.cs b/osu.Game/Screens/Multi/IRoomManager.cs index 3d18edcd71..630e3af91c 100644 --- a/osu.Game/Screens/Multi/IRoomManager.cs +++ b/osu.Game/Screens/Multi/IRoomManager.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Multi /// /// Whether an initial listing of rooms has been received. /// - Bindable InitialRoomsReceived { get; } + IBindable InitialRoomsReceived { get; } /// /// All the active s. diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index a26a64d86d..165a2b201c 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Multi.Lounge protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); - private readonly Bindable initialRoomsReceived = new Bindable(); + private readonly IBindable initialRoomsReceived = new Bindable(); private Container content; private LoadingLayer loadingLayer; From 594db76cf3191ae55861633c1efa187794a018c5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 23:10:45 +0900 Subject: [PATCH 55/87] Fix compilation errors --- osu.Game/Screens/Multi/Components/RoomManager.cs | 5 ++--- .../Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index 0bcf3a90c3..276a5a6148 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -125,8 +125,7 @@ namespace osu.Game.Screens.Multi.Components { if (received == null) { - rooms.Clear(); - initialRoomsReceived.Value = false; + ClearRooms(); return; } @@ -171,7 +170,7 @@ namespace osu.Game.Screens.Multi.Components protected void ClearRooms() { rooms.Clear(); - InitialRoomsReceived.Value = false; + initialRoomsReceived.Value = false; } /// diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 9e3c921d9e..a50628a5fa 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; using osu.Framework.Allocation; @@ -81,7 +82,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer allowPolling.Value = isConnected.Value && JoinedRoom.Value == null; } - protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] + protected override IEnumerable CreatePollingComponents() => new RoomPollingComponent[] { listingPollingComponent = new RealtimeListingPollingComponent { From a25cd910f83fe6ed012fdf63e1f2a20245ac625d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 22:20:35 +0100 Subject: [PATCH 56/87] Prepare base DHO for HO application --- .../Objects/Drawables/DrawableTaikoHitObject.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index cf3aa69b6f..6041eccb51 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; +using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private readonly Container nonProxiedContent; - protected DrawableTaikoHitObject(TaikoHitObject hitObject) + protected DrawableTaikoHitObject([CanBeNull] TaikoHitObject hitObject) : base(hitObject) { AddRangeInternal(new[] @@ -113,25 +113,23 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public override Vector2 OriginPosition => new Vector2(DrawHeight / 2); - public new TObject HitObject; + public new TObject HitObject => (TObject)base.HitObject; protected Vector2 BaseSize; protected SkinnableDrawable MainPiece; - protected DrawableTaikoHitObject(TObject hitObject) + protected DrawableTaikoHitObject([CanBeNull] TObject hitObject) : base(hitObject) { - HitObject = hitObject; - Anchor = Anchor.CentreLeft; Origin = Anchor.Custom; RelativeSizeAxes = Axes.Both; } - [BackgroundDependencyLoader] - private void load() + protected override void OnApply() { + base.OnApply(); RecreatePieces(); } From 7b350fc8e5f61644d63700c236fe2c447eb39efd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 22:27:45 +0100 Subject: [PATCH 57/87] Prepare strongable DHO for HO application --- .../DrawableTaikoStrongableHitObject.cs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs index af3e94d9c6..62f338ca91 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using osu.Framework.Allocation; +using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Audio; @@ -16,28 +16,38 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables where TObject : TaikoStrongableHitObject where TStrongNestedObject : StrongNestedHitObject { - private readonly Bindable isStrong; + private readonly Bindable isStrong = new BindableBool(); private readonly Container strongHitContainer; - protected DrawableTaikoStrongableHitObject(TObject hitObject) + protected DrawableTaikoStrongableHitObject([CanBeNull] TObject hitObject) : base(hitObject) { - isStrong = HitObject.IsStrongBindable.GetBoundCopy(); - AddInternal(strongHitContainer = new Container()); } - [BackgroundDependencyLoader] - private void load() + protected override void OnApply() { + isStrong.BindTo(HitObject.IsStrongBindable); isStrong.BindValueChanged(_ => { - // will overwrite samples, should only be called on change. + // will overwrite samples, should only be called on subsequent changes + // after the initial application. updateSamplesFromStrong(); RecreatePieces(); }); + + base.OnApply(); + } + + protected override void OnFree() + { + base.OnFree(); + + isStrong.UnbindFrom(HitObject.IsStrongBindable); + // ensure the next application does not accidentally overwrite samples. + isStrong.UnbindEvents(); } private HitSampleInfo[] getStrongSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray(); From a31e8d137f3c0bf2926e603a9ef294eb0f8f47c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 22:53:00 +0100 Subject: [PATCH 58/87] Add guard when clearing samples --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 644c67ea59..da6da0ea97 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -303,7 +303,8 @@ namespace osu.Game.Rulesets.Objects.Drawables samplesBindable.CollectionChanged -= onSamplesChanged; // Release the samples for other hitobjects to use. - Samples.Samples = null; + if (Samples != null) + Samples.Samples = null; if (nestedHitObjects.IsValueCreated) { From 232c0205b4d7927c3b84dac00a719ec2fa4be7ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 22:51:35 +0100 Subject: [PATCH 59/87] Refactor hit object application scene to work reliably --- .../HitObjectApplicationTestScene.cs | 14 ++++++++++---- .../TestSceneBarLineApplication.cs | 7 ++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs index 07c7b4d1db..a1d000386f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs +++ b/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs @@ -25,16 +25,22 @@ namespace osu.Game.Rulesets.Taiko.Tests private ScrollingHitObjectContainer hitObjectContainer; - [SetUpSteps] - public void SetUp() - => AddStep("create SHOC", () => Child = hitObjectContainer = new ScrollingHitObjectContainer + [BackgroundDependencyLoader] + private void load() + { + Child = hitObjectContainer = new ScrollingHitObjectContainer { RelativeSizeAxes = Axes.X, Height = 200, Anchor = Anchor.Centre, Origin = Anchor.Centre, Clock = new FramedClock(new StopwatchClock()) - }); + }; + } + + [SetUpSteps] + public void SetUp() + => AddStep("clear SHOC", () => hitObjectContainer.Clear(false)); protected void AddHitObject(DrawableHitObject hitObject) => AddStep("add to SHOC", () => hitObjectContainer.Add(hitObject)); diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs index 65230a07bc..a970965141 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs @@ -12,12 +12,13 @@ namespace osu.Game.Rulesets.Taiko.Tests [Test] public void TestApplyNewBarLine() { - DrawableBarLine barLine = new DrawableBarLine(PrepareObject(new BarLine + DrawableBarLine barLine = new DrawableBarLine(); + + AddStep("apply new bar line", () => barLine.Apply(PrepareObject(new BarLine { StartTime = 400, Major = true - })); - + }), null)); AddHitObject(barLine); RemoveHitObject(barLine); From e32b1c34cac0436c39a5087896f386af30239d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 22:53:46 +0100 Subject: [PATCH 60/87] Implement hit application --- .../TestSceneHitApplication.cs | 37 ++++++++++++++++ .../Objects/Drawables/DrawableHit.cs | 44 ++++++++++++++----- 2 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs new file mode 100644 index 0000000000..52fd440857 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public class TestSceneHitApplication : HitObjectApplicationTestScene + { + [Test] + public void TestApplyNewHit() + { + var hit = new DrawableHit(); + + AddStep("apply new hit", () => hit.Apply(PrepareObject(new Hit + { + Type = HitType.Rim, + IsStrong = false, + StartTime = 300 + }), null)); + + AddHitObject(hit); + RemoveHitObject(hit); + + AddStep("apply new hit", () => hit.Apply(PrepareObject(new Hit + { + Type = HitType.Centre, + IsStrong = true, + StartTime = 500 + }), null)); + + AddHitObject(hit); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 431f2980ec..5a479e1f53 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using osu.Framework.Allocation; +using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Audio; @@ -36,29 +36,51 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private bool pressHandledThisFrame; - private readonly Bindable type; + private readonly Bindable type = new Bindable(); - public DrawableHit(Hit hit) - : base(hit) + public DrawableHit() + : this(null) { - type = HitObject.TypeBindable.GetBoundCopy(); - FillMode = FillMode.Fit; - - updateActionsFromType(); } - [BackgroundDependencyLoader] - private void load() + public DrawableHit([CanBeNull] Hit hit) + : base(hit) { + FillMode = FillMode.Fit; + } + + protected override void OnApply() + { + type.BindTo(HitObject.TypeBindable); type.BindValueChanged(_ => { updateActionsFromType(); - // will overwrite samples, should only be called on change. + // will overwrite samples, should only be called on subsequent changes + // after the initial application. updateSamplesFromTypeChange(); RecreatePieces(); }); + + // action update also has to happen immediately on application. + updateActionsFromType(); + + base.OnApply(); + } + + protected override void OnFree() + { + base.OnFree(); + + type.UnbindFrom(HitObject.TypeBindable); + type.UnbindEvents(); + + UnproxyContent(); + + HitActions = null; + HitAction = null; + validActionPressed = pressHandledThisFrame = false; } private HitSampleInfo[] getRimSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray(); From 8b6bc09b8f0c0dd48879fe05adb5f614aeb4dc29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 23:02:33 +0100 Subject: [PATCH 61/87] Implement drum roll application --- .../TestSceneDrumRollApplication.cs | 39 +++++++++++++++++++ .../Objects/Drawables/DrawableDrumRoll.cs | 34 ++++++++++------ 2 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs new file mode 100644 index 0000000000..54450e27db --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public class TestSceneDrumRollApplication : HitObjectApplicationTestScene + { + [Test] + public void TestApplyNewDrumRoll() + { + var drumRoll = new DrawableDrumRoll(); + + AddStep("apply new drum roll", () => drumRoll.Apply(PrepareObject(new DrumRoll + { + StartTime = 300, + Duration = 500, + IsStrong = false, + TickRate = 2 + }), null)); + + AddHitObject(drumRoll); + RemoveHitObject(drumRoll); + + AddStep("apply new drum roll", () => drumRoll.Apply(PrepareObject(new DrumRoll + { + StartTime = 150, + Duration = 400, + IsStrong = true, + TickRate = 16 + }), null)); + + AddHitObject(drumRoll); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 4925b6fdfc..ede7453804 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Utils; using osu.Game.Graphics; @@ -31,15 +32,26 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// private int rollingHits; - private Container tickContainer; + private readonly Container tickContainer; private Color4 colourIdle; private Color4 colourEngaged; - public DrawableDrumRoll(DrumRoll drumRoll) + public DrawableDrumRoll() + : this(null) + { + } + + public DrawableDrumRoll([CanBeNull] DrumRoll drumRoll) : base(drumRoll) { RelativeSizeAxes = Axes.Y; + + Content.Add(tickContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Depth = float.MinValue + }); } [BackgroundDependencyLoader] @@ -47,12 +59,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { colourIdle = colours.YellowDark; colourEngaged = colours.YellowDarker; - - Content.Add(tickContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Depth = float.MinValue - }); } protected override void LoadComplete() @@ -68,6 +74,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables updateColour(); } + protected override void OnFree() + { + base.OnFree(); + rollingHits = 0; + } + protected override void AddNestedHitObject(DrawableHitObject hitObject) { base.AddNestedHitObject(hitObject); @@ -114,7 +126,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables rollingHits = Math.Clamp(rollingHits, 0, rolling_hits_for_engaged_colour); - updateColour(); + updateColour(100); } protected override void CheckForResult(bool userTriggered, double timeOffset) @@ -156,10 +168,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRoll.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this); - private void updateColour() + private void updateColour(double fadeDuration = 0) { Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1); - (MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, 100); + (MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration); } private class StrongNestedHit : DrawableStrongNestedHit From e3b6eaa39059ba8afc2e71a0d034bc3b227fe49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 23:06:22 +0100 Subject: [PATCH 62/87] Implement swell application Also removes a weird sizing application that seems to have no effect (introduced in 27e63eb; compare removals for other taiko DHO types in 9d00e5b and 58bf288). --- .../Objects/Drawables/DrawableSwell.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 229d581d0c..9798a79450 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -35,7 +36,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; - public DrawableSwell(Swell swell) + public DrawableSwell() + : this(null) + { + } + + public DrawableSwell([CanBeNull] Swell swell) : base(swell) { FillMode = FillMode.Fit; @@ -123,12 +129,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Origin = Anchor.Centre, }); - protected override void LoadComplete() + protected override void OnFree() { - base.LoadComplete(); + base.OnFree(); - // We need to set this here because RelativeSizeAxes won't/can't set our size by default with a different RelativeChildSize - Width *= Parent.RelativeChildSize.X; + UnproxyContent(); + + lastWasCentre = null; } protected override void AddNestedHitObject(DrawableHitObject hitObject) From 3bd42795899eda793b9b0d346abdae7902585ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 23:10:19 +0100 Subject: [PATCH 63/87] Implement drum roll tick application --- .../Objects/Drawables/DrawableDrumRollTick.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index c6761de5e3..e7d8ef1e12 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Skinning.Default; @@ -16,7 +17,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// public HitType JudgementType; - public DrawableDrumRollTick(DrumRollTick tick) + public DrawableDrumRollTick() + : this(null) + { + } + + public DrawableDrumRollTick([CanBeNull] DrumRollTick tick) : base(tick) { FillMode = FillMode.Fit; From d823c77a63e315207bbe7f282ef07ef3eeb8bf1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 23:11:51 +0100 Subject: [PATCH 64/87] Implement swell tick application --- .../Objects/Drawables/DrawableSwellTick.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs index 14c86d151f..47fc7e5ab3 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.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 JetBrains.Annotations; using osu.Framework.Graphics; using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Skinning; @@ -11,7 +12,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public override bool DisplayResult => false; - public DrawableSwellTick(SwellTick hitObject) + public DrawableSwellTick() + : this(null) + { + } + + public DrawableSwellTick([CanBeNull] SwellTick hitObject) : base(hitObject) { } From ae6dedacaf1edb784c9ca7bdfb4a871f6e0821f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 23:21:19 +0100 Subject: [PATCH 65/87] Implement nested strong hit application --- .../Objects/Drawables/DrawableDrumRoll.cs | 17 ++++++++---- .../Objects/Drawables/DrawableDrumRollTick.cs | 17 ++++++++---- .../Objects/Drawables/DrawableHit.cs | 27 +++++++++++-------- .../Drawables/DrawableStrongNestedHit.cs | 7 +++-- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 2 +- 5 files changed, 44 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index ede7453804..01336ea2e4 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -166,7 +166,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Content.X = DrawHeight / 2; } - protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRoll.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this); + protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRoll.StrongNestedHit hitObject) => new StrongNestedHit(hitObject); private void updateColour(double fadeDuration = 0) { @@ -176,17 +176,24 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private class StrongNestedHit : DrawableStrongNestedHit { - public StrongNestedHit(DrumRoll.StrongNestedHit nestedHit, DrawableDrumRoll drumRoll) - : base(nestedHit, drumRoll) + public new DrawableDrumRoll ParentHitObject => (DrawableDrumRoll)base.ParentHitObject; + + public StrongNestedHit() + : this(null) + { + } + + public StrongNestedHit([CanBeNull] DrumRoll.StrongNestedHit nestedHit) + : base(nestedHit) { } protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (!MainObject.Judged) + if (!ParentHitObject.Judged) return; - ApplyResult(r => r.Type = MainObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); + ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); } public override bool OnPressed(TaikoAction action) => false; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index e7d8ef1e12..1e625d91d6 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -67,21 +67,28 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return UpdateResult(true); } - protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRollTick.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this); + protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRollTick.StrongNestedHit hitObject) => new StrongNestedHit(hitObject); private class StrongNestedHit : DrawableStrongNestedHit { - public StrongNestedHit(DrumRollTick.StrongNestedHit nestedHit, DrawableDrumRollTick tick) - : base(nestedHit, tick) + public new DrawableDrumRollTick ParentHitObject => (DrawableDrumRollTick)base.ParentHitObject; + + public StrongNestedHit() + : this(null) + { + } + + public StrongNestedHit([CanBeNull] DrumRollTick.StrongNestedHit nestedHit) + : base(nestedHit) { } protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (!MainObject.Judged) + if (!ParentHitObject.Judged) return; - ApplyResult(r => r.Type = MainObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); + ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); } public override bool OnPressed(TaikoAction action) => false; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 5a479e1f53..73ebd7c117 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -250,32 +250,37 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - protected override DrawableStrongNestedHit CreateStrongNestedHit(Hit.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this); + protected override DrawableStrongNestedHit CreateStrongNestedHit(Hit.StrongNestedHit hitObject) => new StrongNestedHit(hitObject); private class StrongNestedHit : DrawableStrongNestedHit { + public new DrawableHit ParentHitObject => (DrawableHit)base.ParentHitObject; + /// /// The lenience for the second key press. /// This does not adjust by map difficulty in ScoreV2 yet. /// private const double second_hit_window = 30; - public new DrawableHit MainObject => (DrawableHit)base.MainObject; + public StrongNestedHit() + : this(null) + { + } - public StrongNestedHit(Hit.StrongNestedHit nestedHit, DrawableHit hit) - : base(nestedHit, hit) + public StrongNestedHit([CanBeNull] Hit.StrongNestedHit nestedHit) + : base(nestedHit) { } protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (!MainObject.Result.HasResult) + if (!ParentHitObject.Result.HasResult) { base.CheckForResult(userTriggered, timeOffset); return; } - if (!MainObject.Result.IsHit) + if (!ParentHitObject.Result.IsHit) { ApplyResult(r => r.Type = r.Judgement.MinResult); return; @@ -283,27 +288,27 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!userTriggered) { - if (timeOffset - MainObject.Result.TimeOffset > second_hit_window) + if (timeOffset - ParentHitObject.Result.TimeOffset > second_hit_window) ApplyResult(r => r.Type = r.Judgement.MinResult); return; } - if (Math.Abs(timeOffset - MainObject.Result.TimeOffset) <= second_hit_window) + if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= second_hit_window) ApplyResult(r => r.Type = r.Judgement.MaxResult); } public override bool OnPressed(TaikoAction action) { // Don't process actions until the main hitobject is hit - if (!MainObject.IsHit) + if (!ParentHitObject.IsHit) return false; // Don't process actions if the pressed button was released - if (MainObject.HitAction == null) + if (ParentHitObject.HitAction == null) return false; // Don't handle invalid hit action presses - if (!MainObject.HitActions.Contains(action)) + if (!ParentHitObject.HitActions.Contains(action)) return false; return UpdateResult(true); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index d2e8888197..9c22e34387 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs @@ -1,7 +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 osu.Game.Rulesets.Objects.Drawables; +using JetBrains.Annotations; using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -11,12 +11,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// public abstract class DrawableStrongNestedHit : DrawableTaikoHitObject { - public readonly DrawableHitObject MainObject; + public new DrawableTaikoHitObject ParentHitObject => (DrawableTaikoHitObject)base.ParentHitObject; - protected DrawableStrongNestedHit(StrongNestedHitObject nestedHit, DrawableHitObject mainObject) + protected DrawableStrongNestedHit([CanBeNull] StrongNestedHitObject nestedHit) : base(nestedHit) { - MainObject = mainObject; } } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index d20b190c86..8682495b41 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -248,7 +248,7 @@ namespace osu.Game.Rulesets.Taiko.UI { case TaikoStrongJudgement _: if (result.IsHit) - hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).MainObject)?.VisualiseSecondHit(); + hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).ParentHitObject)?.VisualiseSecondHit(); break; case TaikoDrumRollTickJudgement _: From d127494c2dedfd133012e574f7fe68d15049073d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 00:39:31 +0900 Subject: [PATCH 66/87] Fix thread-unsafe room removal --- .../Multi/RealtimeMultiplayer/RealtimeRoomManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index a50628a5fa..734d00b9aa 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -54,9 +54,12 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer multiplayerClient.LeaveRoom().Wait(); // Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case. - RemoveRoom(joinedRoom); // This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling. - Schedule(() => listingPollingComponent.PollImmediately()); + Schedule(() => + { + RemoveRoom(joinedRoom); + listingPollingComponent.PollImmediately(); + }); } private void joinMultiplayerRoom(Room room, Action onSuccess = null) From a893360c0e07d3c2b31206ace178ad33cf85a47f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 00:41:14 +0900 Subject: [PATCH 67/87] Reword comment --- .../Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 8d3b161804..0065b425ec 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -71,7 +71,9 @@ namespace osu.Game.Online.RealtimeMultiplayer private RulesetStore rulesets { get; set; } = null!; private Room? apiRoom; - private int playlistItemId; // Todo: THIS IS SUPER TEMPORARY!! + + // Todo: This is temporary, until the multiplayer server returns the item id on match start or otherwise. + private int playlistItemId; /// /// Joins the for a given API . From 0c5333bd586dfea1f40e3f6e6d4144bd887713a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Dec 2020 17:57:19 +0100 Subject: [PATCH 68/87] Adjust top-level hitobjects to support nested pooling --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs | 2 +- .../Objects/Drawables/DrawableTaikoStrongableHitObject.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 01336ea2e4..d085b95f35 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); - tickContainer.Clear(); + tickContainer.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 9798a79450..60f9521996 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); - ticks.Clear(); + ticks.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs index 62f338ca91..4f1523eb3f 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs @@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); - strongHitContainer.Clear(); + strongHitContainer.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) From 370f56eadbb97aa1ed05d7bc1952e3b35924430f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Dec 2020 18:02:31 +0100 Subject: [PATCH 69/87] Make strong hit DHOs public for pool registration --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs | 2 +- .../Objects/Drawables/DrawableDrumRollTick.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index d085b95f35..d066abf767 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables (MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration); } - private class StrongNestedHit : DrawableStrongNestedHit + public class StrongNestedHit : DrawableStrongNestedHit { public new DrawableDrumRoll ParentHitObject => (DrawableDrumRoll)base.ParentHitObject; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 1e625d91d6..0df45c424d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRollTick.StrongNestedHit hitObject) => new StrongNestedHit(hitObject); - private class StrongNestedHit : DrawableStrongNestedHit + public class StrongNestedHit : DrawableStrongNestedHit { public new DrawableDrumRollTick ParentHitObject => (DrawableDrumRollTick)base.ParentHitObject; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 73ebd7c117..38cda69a46 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -252,7 +252,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override DrawableStrongNestedHit CreateStrongNestedHit(Hit.StrongNestedHit hitObject) => new StrongNestedHit(hitObject); - private class StrongNestedHit : DrawableStrongNestedHit + public class StrongNestedHit : DrawableStrongNestedHit { public new DrawableHit ParentHitObject => (DrawableHit)base.ParentHitObject; From 62da4eff37db8dfc1cfeaeac7d35190eab54356a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Dec 2020 18:07:59 +0100 Subject: [PATCH 70/87] Route new result callback via playfield Follows route taken by osu! and catch (and required for proper pooling support). --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 8682495b41..6b001d6c70 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -149,6 +149,12 @@ namespace osu.Game.Rulesets.Taiko.UI }; } + protected override void LoadComplete() + { + base.LoadComplete(); + NewResult += OnNewResult; + } + protected override void Update() { base.Update(); @@ -208,7 +214,6 @@ namespace osu.Game.Rulesets.Taiko.UI break; case DrawableTaikoHitObject taikoObject: - h.OnNewResult += OnNewResult; topLevelHitContainer.Add(taikoObject.CreateProxiedContent()); base.Add(h); break; @@ -226,7 +231,6 @@ namespace osu.Game.Rulesets.Taiko.UI return barLinePlayfield.Remove(barLine); case DrawableTaikoHitObject _: - h.OnNewResult -= OnNewResult; // todo: consider tidying of proxied content if required. return base.Remove(h); From 5d575d2a9b8dd95d8c06e9ef9f1b91214b7794d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Dec 2020 18:11:29 +0100 Subject: [PATCH 71/87] Accept proxied content via OnNewDrawableHitObject In the non-pooled case, `OnNewDrawableHitObject()` will be called automatically on each new DHO via `Playfield.Add(DrawableHitObject)`. In the pooled case, it will be called via `Playfield`'s implementation of `GetPooledDrawableRepresentation(HitObject, DrawableHitObject)`. --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 6b001d6c70..b3bf0974c3 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -155,6 +155,14 @@ namespace osu.Game.Rulesets.Taiko.UI NewResult += OnNewResult; } + protected override void OnNewDrawableHitObject(DrawableHitObject drawableHitObject) + { + base.OnNewDrawableHitObject(drawableHitObject); + + var taikoObject = (DrawableTaikoHitObject)drawableHitObject; + topLevelHitContainer.Add(taikoObject.CreateProxiedContent()); + } + protected override void Update() { base.Update(); @@ -213,8 +221,7 @@ namespace osu.Game.Rulesets.Taiko.UI barLinePlayfield.Add(barLine); break; - case DrawableTaikoHitObject taikoObject: - topLevelHitContainer.Add(taikoObject.CreateProxiedContent()); + case DrawableTaikoHitObject _: base.Add(h); break; @@ -231,7 +238,6 @@ namespace osu.Game.Rulesets.Taiko.UI return barLinePlayfield.Remove(barLine); case DrawableTaikoHitObject _: - // todo: consider tidying of proxied content if required. return base.Remove(h); default: From b24fc1922e11b103243ef24e361609376c353b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Dec 2020 18:16:05 +0100 Subject: [PATCH 72/87] Enable pooling for taiko DHOs --- .../UI/DrawableTaikoRuleset.cs | 18 +----------------- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 12 ++++++++++++ 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index 9cf931ee0a..ed8e6859a2 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Taiko.Replays; using osu.Framework.Input; @@ -64,22 +63,7 @@ namespace osu.Game.Rulesets.Taiko.UI protected override Playfield CreatePlayfield() => new TaikoPlayfield(Beatmap.ControlPointInfo); - public override DrawableHitObject CreateDrawableRepresentation(TaikoHitObject h) - { - switch (h) - { - case Hit hit: - return new DrawableHit(hit); - - case DrumRoll drumRoll: - return new DrawableDrumRoll(drumRoll); - - case Swell swell: - return new DrawableSwell(swell); - } - - return null; - } + public override DrawableHitObject CreateDrawableRepresentation(TaikoHitObject h) => null; protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new TaikoFramedReplayInputHandler(replay); diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index b3bf0974c3..148ec7755e 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -147,6 +147,18 @@ namespace osu.Game.Rulesets.Taiko.UI }, drumRollHitContainer.CreateProxy(), }; + + RegisterPool(50); + RegisterPool(50); + + RegisterPool(5); + RegisterPool(5); + + RegisterPool(100); + RegisterPool(100); + + RegisterPool(5); + RegisterPool(100); } protected override void LoadComplete() From 6e21806873976561322499602e516ee0a6c29d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Dec 2020 18:25:49 +0100 Subject: [PATCH 73/87] Adjust sample test to pass with pooling --- .../TestSceneSampleOutput.cs | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs index 4ba9c447fb..296468d98d 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.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.Collections.Generic; using System.Linq; using osu.Framework.Testing; using osu.Game.Audio; @@ -18,24 +19,33 @@ namespace osu.Game.Rulesets.Taiko.Tests public override void SetUpSteps() { base.SetUpSteps(); - AddAssert("has correct samples", () => + + var expectedSampleNames = new[] { - var names = Player.DrawableRuleset.Playfield.AllHitObjects.OfType().Select(h => string.Join(',', h.GetSamples().Select(s => s.Name))); + string.Empty, + string.Empty, + string.Empty, + string.Empty, + HitSampleInfo.HIT_FINISH, + HitSampleInfo.HIT_WHISTLE, + HitSampleInfo.HIT_WHISTLE, + HitSampleInfo.HIT_WHISTLE, + }; + var actualSampleNames = new List(); - var expected = new[] - { - string.Empty, - string.Empty, - string.Empty, - string.Empty, - HitSampleInfo.HIT_FINISH, - HitSampleInfo.HIT_WHISTLE, - HitSampleInfo.HIT_WHISTLE, - HitSampleInfo.HIT_WHISTLE, - }; + // due to pooling we can't access all samples right away due to object re-use, + // so we need to collect as we go. + AddStep("collect sample names", () => Player.DrawableRuleset.Playfield.NewResult += (dho, _) => + { + if (!(dho is DrawableHit h)) + return; - return names.SequenceEqual(expected); + actualSampleNames.Add(string.Join(',', h.GetSamples().Select(s => s.Name))); }); + + AddUntilStep("all samples collected", () => actualSampleNames.Count == expectedSampleNames.Length); + + AddAssert("samples are correct", () => actualSampleNames.SequenceEqual(expectedSampleNames)); } protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TaikoBeatmapConversionTest().GetBeatmap("sample-to-type-conversions"); From a8569fe15cdf663685d960e69356205b11944f8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Dec 2020 13:35:46 +0900 Subject: [PATCH 74/87] Fix a couple of simple cases of incorrect TextureLoaderStore initialisation --- osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs | 3 ++- osu.Game/Storyboards/Drawables/DrawableStoryboard.cs | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index a9b2a15b35..b13b20dae2 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -13,6 +13,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Framework.Platform; using osu.Game.Rulesets.Configuration; namespace osu.Game.Rulesets.UI @@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.UI if (resources != null) { - TextureStore = new TextureStore(new TextureLoaderStore(new NamespacedResourceStore(resources, @"Textures"))); + TextureStore = new TextureStore(parent.Get().CreateTextureLoaderStore(new NamespacedResourceStore(resources, @"Textures"))); CacheAs(TextureStore = new FallbackTextureStore(TextureStore, parent.Get())); SampleStore = parent.Get().GetSampleStore(new NamespacedResourceStore(resources, @"Samples")); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 4bc28e6cef..af4615c895 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; +using osu.Framework.Platform; using osu.Game.IO; using osu.Game.Screens.Play; @@ -59,12 +60,12 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader(true)] - private void load(FileStore fileStore, GameplayClock clock, CancellationToken? cancellationToken) + private void load(FileStore fileStore, GameplayClock clock, CancellationToken? cancellationToken, GameHost host) { if (clock != null) Clock = clock; - dependencies.Cache(new TextureStore(new TextureLoaderStore(fileStore.Store), false, scaleAdjust: 1)); + dependencies.Cache(new TextureStore(host.CreateTextureLoaderStore(fileStore.Store), false, scaleAdjust: 1)); foreach (var layer in Storyboard.Layers) { From 82cf58353ce15e45770bea510181769abeaee846 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 15:38:20 +0900 Subject: [PATCH 75/87] Fix incorrect joinedroom null checks --- osu.Game/Screens/Multi/Components/RoomManager.cs | 2 +- .../Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index 276a5a6148..482ee5492c 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -112,7 +112,7 @@ namespace osu.Game.Screens.Multi.Components { currentJoinRoomRequest?.Cancel(); - if (JoinedRoom == null) + if (JoinedRoom.Value == null) return; api.Queue(new PartRoomRequest(joinedRoom.Value)); diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index b81ca7aade..e0fca3ce4c 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer public override void PartRoom() { - if (JoinedRoom == null) + if (JoinedRoom.Value == null) return; var joinedRoom = JoinedRoom.Value; From a59124dd938a8c98a276c85299cbbb144f87b190 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 16:18:39 +0900 Subject: [PATCH 76/87] Make room duration/endsat nullable --- .../Visual/Multiplayer/TestSceneRoomStatus.cs | 12 +++++++++--- osu.Game/Online/Multiplayer/Room.cs | 16 +++++++++++----- .../Screens/Multi/Components/RoomStatusInfo.cs | 11 +++++++++-- .../Match/Components/MatchSettingsOverlay.cs | 2 +- osu.Game/Screens/Multi/MultiplayerComposite.cs | 4 ++-- .../Multi/Timeshift/TimeshiftReadyButton.cs | 4 ++-- 6 files changed, 34 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs index c1dfb94464..a6dd1437f7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs @@ -22,22 +22,28 @@ namespace osu.Game.Tests.Visual.Multiplayer { new DrawableRoom(new Room { - Name = { Value = "Room 1" }, + Name = { Value = "Open - ending in 1 day" }, Status = { Value = new RoomStatusOpen() }, EndDate = { Value = DateTimeOffset.Now.AddDays(1) } }) { MatchingFilter = true }, new DrawableRoom(new Room { - Name = { Value = "Room 2" }, + Name = { Value = "Playing - ending in 1 day" }, Status = { Value = new RoomStatusPlaying() }, EndDate = { Value = DateTimeOffset.Now.AddDays(1) } }) { MatchingFilter = true }, new DrawableRoom(new Room { - Name = { Value = "Room 3" }, + Name = { Value = "Ended" }, Status = { Value = new RoomStatusEnded() }, EndDate = { Value = DateTimeOffset.Now } }) { MatchingFilter = true }, + new DrawableRoom(new Room + { + Name = { Value = "Open (realtime)" }, + Status = { Value = new RoomStatusOpen() }, + Category = { Value = RoomCategory.Realtime } + }) { MatchingFilter = true }, } }; } diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index 9a21543b2e..ee8992e399 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -40,7 +40,7 @@ namespace osu.Game.Online.Multiplayer [Cached] [JsonIgnore] - public readonly Bindable Duration = new Bindable(TimeSpan.FromMinutes(30)); + public readonly Bindable Duration = new Bindable(); [Cached] [JsonIgnore] @@ -78,16 +78,22 @@ namespace osu.Game.Online.Multiplayer } [JsonProperty("duration")] - private int duration + private int? duration { - get => (int)Duration.Value.TotalMinutes; - set => Duration.Value = TimeSpan.FromMinutes(value); + get => (int?)Duration.Value?.TotalMinutes; + set + { + if (value == null) + Duration.Value = null; + else + Duration.Value = TimeSpan.FromMinutes(value.Value); + } } // Only supports retrieval for now [Cached] [JsonProperty("ends_at")] - public readonly Bindable EndDate = new Bindable(); + public readonly Bindable EndDate = new Bindable(); // Todo: Find a better way to do this (https://github.com/ppy/osu-framework/issues/1930) [JsonProperty("max_attempts", DefaultValueHandling = DefaultValueHandling.Ignore)] diff --git a/osu.Game/Screens/Multi/Components/RoomStatusInfo.cs b/osu.Game/Screens/Multi/Components/RoomStatusInfo.cs index d799f846c2..b5676692a4 100644 --- a/osu.Game/Screens/Multi/Components/RoomStatusInfo.cs +++ b/osu.Game/Screens/Multi/Components/RoomStatusInfo.cs @@ -48,16 +48,23 @@ namespace osu.Game.Screens.Multi.Components private class EndDatePart : DrawableDate { - public readonly IBindable EndDate = new Bindable(); + public readonly IBindable EndDate = new Bindable(); public EndDatePart() : base(DateTimeOffset.UtcNow) { - EndDate.BindValueChanged(date => Date = date.NewValue); + EndDate.BindValueChanged(date => + { + // If null, set a very large future date to prevent unnecessary schedules. + Date = date.NewValue ?? DateTimeOffset.Now.AddYears(1); + }, true); } protected override string Format() { + if (EndDate.Value == null) + return string.Empty; + var diffToNow = Date.Subtract(DateTimeOffset.Now); if (diffToNow.TotalSeconds < -5) diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs index b8003b9774..1859e8db8a 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs @@ -325,7 +325,7 @@ namespace osu.Game.Screens.Multi.Match.Components Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true); Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true); MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true); - Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue, true); + Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true); playlist.Items.BindTo(Playlist); Playlist.BindCollectionChanged(onPlaylistChanged, true); diff --git a/osu.Game/Screens/Multi/MultiplayerComposite.cs b/osu.Game/Screens/Multi/MultiplayerComposite.cs index e612e77748..6e0c69d712 100644 --- a/osu.Game/Screens/Multi/MultiplayerComposite.cs +++ b/osu.Game/Screens/Multi/MultiplayerComposite.cs @@ -40,12 +40,12 @@ namespace osu.Game.Screens.Multi protected Bindable MaxParticipants { get; private set; } [Resolved(typeof(Room))] - protected Bindable EndDate { get; private set; } + protected Bindable EndDate { get; private set; } [Resolved(typeof(Room))] protected Bindable Availability { get; private set; } [Resolved(typeof(Room))] - protected Bindable Duration { get; private set; } + protected Bindable Duration { get; private set; } } } diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs index ba639c29f4..c878451eee 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.Multi.Timeshift public class TimeshiftReadyButton : ReadyButton { [Resolved(typeof(Room), nameof(Room.EndDate))] - private Bindable endDate { get; set; } + private Bindable endDate { get; set; } public TimeshiftReadyButton() { @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Multi.Timeshift { base.Update(); - Enabled.Value = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(GameBeatmap.Value.Track.Length) < endDate.Value; + Enabled.Value = endDate.Value != null && DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(GameBeatmap.Value.Track.Length) < endDate.Value; } } } From c3d1eaf36dec71ff703f623b1f274192e6f1275c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 16:21:05 +0900 Subject: [PATCH 77/87] Make RealtimeMultiplayerTestScene abstract --- .../RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs index b52106551e..aec70d8be4 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs @@ -12,7 +12,7 @@ using osu.Game.Screens.Multi.RealtimeMultiplayer; namespace osu.Game.Tests.Visual.RealtimeMultiplayer { - public class RealtimeMultiplayerTestScene : MultiplayerTestScene + public abstract class RealtimeMultiplayerTestScene : MultiplayerTestScene { [Cached(typeof(StatefulMultiplayerClient))] public TestRealtimeMultiplayerClient Client { get; } @@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer private readonly bool joinRoom; - public RealtimeMultiplayerTestScene(bool joinRoom = true) + protected RealtimeMultiplayerTestScene(bool joinRoom = true) { this.joinRoom = joinRoom; base.Content.Add(content = new TestRealtimeRoomContainer { RelativeSizeAxes = Axes.Both }); From 64a32723f3b544fec8f02b120f00986ffcd3e65e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 16:23:42 +0900 Subject: [PATCH 78/87] One more case --- osu.Game/Online/Multiplayer/Room.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index ee8992e399..e3444d6b7e 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -139,7 +139,7 @@ namespace osu.Game.Online.Multiplayer ParticipantCount.Value = other.ParticipantCount.Value; EndDate.Value = other.EndDate.Value; - if (DateTimeOffset.Now >= EndDate.Value) + if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value) Status.Value = new RoomStatusEnded(); if (!Playlist.SequenceEqual(other.Playlist)) From 5d73359bd7c0e95a9e18c202319d0ecf77071e18 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 16:35:19 +0900 Subject: [PATCH 79/87] Make participant count non-nullable --- osu.Game/Online/Multiplayer/Room.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index 9a21543b2e..5930a624bc 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -67,15 +67,8 @@ namespace osu.Game.Online.Multiplayer public readonly BindableList RecentParticipants = new BindableList(); [Cached] - public readonly Bindable ParticipantCount = new Bindable(); - - // todo: TEMPORARY [JsonProperty("participant_count")] - private int? participantCount - { - get => ParticipantCount.Value; - set => ParticipantCount.Value = value ?? 0; - } + public readonly Bindable ParticipantCount = new Bindable(); [JsonProperty("duration")] private int duration From d096f2f8f6445eb3524df9612e8d7a891528bebe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Dec 2020 16:39:46 +0900 Subject: [PATCH 80/87] Fix potential cross-thread operation during chat channel load The callbacks are scheduled to the API thread, but hooked up in BDL load. This causes a potential case of cross-thread collection enumeration. I've tested and it seems like the schedule logic should be fine for short term. Longer term, we probably want to re-think how this works so background operations aren't performed on the `DrawableChannel` in the first place (chat shouldn't have an overhead like this when not visible). Closes #11231. --- osu.Game/Overlays/Chat/DrawableChannel.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index d63faebae4..5926d11c03 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -103,7 +103,7 @@ namespace osu.Game.Overlays.Chat Colour = colours.ChatBlue.Lighten(0.7f), }; - private void newMessagesArrived(IEnumerable newMessages) + private void newMessagesArrived(IEnumerable newMessages) => Schedule(() => { if (newMessages.Min(m => m.Id) < chatLines.Max(c => c.Message.Id)) { @@ -155,9 +155,9 @@ namespace osu.Game.Overlays.Chat if (shouldScrollToEnd) scrollToEnd(); - } + }); - private void pendingMessageResolved(Message existing, Message updated) + private void pendingMessageResolved(Message existing, Message updated) => Schedule(() => { var found = chatLines.LastOrDefault(c => c.Message == existing); @@ -169,12 +169,12 @@ namespace osu.Game.Overlays.Chat found.Message = updated; ChatLineFlow.Add(found); } - } + }); - private void messageRemoved(Message removed) + private void messageRemoved(Message removed) => Schedule(() => { chatLines.FirstOrDefault(c => c.Message == removed)?.FadeColour(Color4.Red, 400).FadeOut(600).Expire(); - } + }); private IEnumerable chatLines => ChatLineFlow.Children.OfType(); From a021aaf54673acf78c5272d077e757b1ded87bc8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 16:42:21 +0900 Subject: [PATCH 81/87] Fix room category being serialised as ints --- osu.Game/Online/Multiplayer/Room.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index 9a21543b2e..53ae142ad4 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -35,9 +35,22 @@ namespace osu.Game.Online.Multiplayer public readonly Bindable ChannelId = new Bindable(); [Cached] - [JsonProperty("category")] + [JsonIgnore] public readonly Bindable Category = new Bindable(); + // Todo: osu-framework bug (https://github.com/ppy/osu-framework/issues/4106) + [JsonProperty("category")] + private string categoryString + { + get => Category.Value.ToString().ToLower(); + set + { + if (!Enum.TryParse(value, true, out var enumValue)) + enumValue = RoomCategory.Normal; + Category.Value = enumValue; + } + } + [Cached] [JsonIgnore] public readonly Bindable Duration = new Bindable(TimeSpan.FromMinutes(30)); From e23d81bfc64594c80d21ea1043ec085a47161f25 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 16:56:45 +0900 Subject: [PATCH 82/87] Use enum property --- .../Converters/SnakeCaseStringEnumConverter.cs | 16 ++++++++++++++++ osu.Game/Online/Multiplayer/Room.cs | 13 +++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.cs diff --git a/osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.cs b/osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.cs new file mode 100644 index 0000000000..1d82a5bc87 --- /dev/null +++ b/osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.cs @@ -0,0 +1,16 @@ +// 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.Converters; +using Newtonsoft.Json.Serialization; + +namespace osu.Game.IO.Serialization.Converters +{ + public class SnakeCaseStringEnumConverter : StringEnumConverter + { + public SnakeCaseStringEnumConverter() + { + NamingStrategy = new SnakeCaseNamingStrategy(); + } + } +} diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index 53ae142ad4..66e3e8975a 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -6,6 +6,7 @@ using System.Linq; using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.IO.Serialization.Converters; using osu.Game.Online.Multiplayer.GameTypes; using osu.Game.Online.Multiplayer.RoomStatuses; using osu.Game.Users; @@ -40,15 +41,11 @@ namespace osu.Game.Online.Multiplayer // Todo: osu-framework bug (https://github.com/ppy/osu-framework/issues/4106) [JsonProperty("category")] - private string categoryString + [JsonConverter(typeof(SnakeCaseStringEnumConverter))] + private RoomCategory category { - get => Category.Value.ToString().ToLower(); - set - { - if (!Enum.TryParse(value, true, out var enumValue)) - enumValue = RoomCategory.Normal; - Category.Value = enumValue; - } + get => Category.Value; + set => Category.Value = value; } [Cached] From eb46c9ce9be47341a18e08fedeeb65408ac5847c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 17:11:30 +0900 Subject: [PATCH 83/87] Fix metadata lost in beatmapset deserialisation --- .../Online/API/Requests/Responses/APIBeatmapSet.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index 6d0160fbc4..720d6bfff4 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -80,7 +80,7 @@ namespace osu.Game.Online.API.Requests.Responses public BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets) { - return new BeatmapSetInfo + var beatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = OnlineBeatmapSetID, Metadata = this, @@ -104,8 +104,17 @@ namespace osu.Game.Online.API.Requests.Responses Genre = genre, Language = language }, - Beatmaps = beatmaps?.Select(b => b.ToBeatmap(rulesets)).ToList(), }; + + beatmapSet.Beatmaps = beatmaps?.Select(b => + { + var beatmap = b.ToBeatmap(rulesets); + beatmap.BeatmapSet = beatmapSet; + beatmap.Metadata = beatmapSet.Metadata; + return beatmap; + }).ToList(); + + return beatmapSet; } } } From 83f1350d7d218ba69082bffe0fc7f495b428cf34 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Dec 2020 17:49:10 +0900 Subject: [PATCH 84/87] Fix editor background not being correctly cleaned up on forced exit Closes #11214. Should be pretty obvious why. --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index ca7e5fbf20..223c678fba 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -461,7 +461,7 @@ namespace osu.Game.Screens.Edit if (dialogOverlay == null || dialogOverlay.CurrentDialog is PromptForSaveDialog) { confirmExit(); - return false; + return base.OnExiting(next); } if (isNewBeatmap || HasUnsavedChanges) From d11d754715220e5e84c991d497e91d561126444a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Dec 2020 18:09:37 +0900 Subject: [PATCH 85/87] Increase size of circle display on timeline --- .../Compose/Components/Timeline/TimelineHitObjectBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 657c5834b2..ae2a82fa10 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { private const float thickness = 5; private const float shadow_radius = 5; - private const float circle_size = 24; + private const float circle_size = 34; public Action OnDragHandled; From d1be7c23d96ec9efe7d4edaaa87fd43c155af259 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Dec 2020 18:09:56 +0900 Subject: [PATCH 86/87] Increase height of timeline drag area --- .../Compose/Components/Timeline/TimelineBlueprintContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 2f14c607c2..ead1aa5c62 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Anchor = Anchor.Centre; Origin = Anchor.Centre; - Height = 0.4f; + Height = 0.6f; AddInternal(new Box { From 423c6158e16bda8c5382de4f99eeabcd776cfb97 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Dec 2020 18:10:11 +0900 Subject: [PATCH 87/87] Highlight timeline drag area when hovered for better visibility --- .../Timeline/TimelineBlueprintContainer.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index ead1aa5c62..1fc529910b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; @@ -25,10 +26,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private EditorBeatmap beatmap { get; set; } + [Resolved] + private OsuColour colours { get; set; } + private DragEvent lastDragEvent; private Bindable placement; private SelectionBlueprint placementBlueprint; + private readonly Box backgroundBox; + public TimelineBlueprintContainer(HitObjectComposer composer) : base(composer) { @@ -38,7 +44,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Height = 0.6f; - AddInternal(new Box + AddInternal(backgroundBox = new Box { Colour = Color4.Black, RelativeSizeAxes = Axes.Both, @@ -77,6 +83,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override Container CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; + protected override bool OnHover(HoverEvent e) + { + backgroundBox.FadeColour(colours.BlueLighter, 120, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + backgroundBox.FadeColour(Color4.Black, 600, Easing.OutQuint); + base.OnHoverLost(e); + } + protected override void OnDrag(DragEvent e) { handleScrollViaDrag(e);