From eecf6ad558983786890f5905370142fcff4d5a75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Nov 2021 21:53:40 +0900 Subject: [PATCH 001/419] Add `IsManaged` helper method to EF classes to match realm implementation --- osu.Game/Beatmaps/BeatmapDifficulty.cs | 2 ++ osu.Game/Beatmaps/BeatmapInfo.cs | 2 ++ osu.Game/Beatmaps/BeatmapMetadata.cs | 2 ++ osu.Game/Beatmaps/BeatmapSetFileInfo.cs | 2 ++ osu.Game/Beatmaps/BeatmapSetInfo.cs | 2 ++ osu.Game/Configuration/DatabasedSetting.cs | 2 ++ osu.Game/Database/IHasPrimaryKey.cs | 2 ++ osu.Game/IO/FileInfo.cs | 2 ++ osu.Game/Scoring/ScoreFileInfo.cs | 2 ++ osu.Game/Scoring/ScoreInfo.cs | 2 ++ osu.Game/Skinning/SkinFileInfo.cs | 2 ++ osu.Game/Skinning/SkinInfo.cs | 2 ++ 12 files changed, 24 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index dfd21469fa..65d1fb8286 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -15,6 +15,8 @@ namespace osu.Game.Beatmaps public int ID { get; set; } + public bool IsManaged => ID > 0; + public float DrainRate { get; set; } = DEFAULT_DIFFICULTY; public float CircleSize { get; set; } = DEFAULT_DIFFICULTY; public float OverallDifficulty { get; set; } = DEFAULT_DIFFICULTY; diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index d2b322a843..7359de0cd7 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -21,6 +21,8 @@ namespace osu.Game.Beatmaps { public int ID { get; set; } + public bool IsManaged => ID > 0; + public int BeatmapVersion; private int? onlineID; diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index b395f16c24..5da0264893 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -20,6 +20,8 @@ namespace osu.Game.Beatmaps { public int ID { get; set; } + public bool IsManaged => ID > 0; + public string Title { get; set; } = string.Empty; [JsonProperty("title_unicode")] diff --git a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs index ce50463f05..29dcf4d6aa 100644 --- a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs @@ -11,6 +11,8 @@ namespace osu.Game.Beatmaps { public int ID { get; set; } + public bool IsManaged => ID > 0; + public int BeatmapSetInfoID { get; set; } public int FileInfoID { get; set; } diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index a0de50a311..db5c3bf15a 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -16,6 +16,8 @@ namespace osu.Game.Beatmaps { public int ID { get; set; } + public bool IsManaged => ID > 0; + private int? onlineID; [Column("OnlineBeatmapSetID")] diff --git a/osu.Game/Configuration/DatabasedSetting.cs b/osu.Game/Configuration/DatabasedSetting.cs index fe1d51d57f..65d9f7799d 100644 --- a/osu.Game/Configuration/DatabasedSetting.cs +++ b/osu.Game/Configuration/DatabasedSetting.cs @@ -11,6 +11,8 @@ namespace osu.Game.Configuration { public int ID { get; set; } + public bool IsManaged => ID > 0; + public int? RulesetID { get; set; } public int? Variant { get; set; } diff --git a/osu.Game/Database/IHasPrimaryKey.cs b/osu.Game/Database/IHasPrimaryKey.cs index 3c0fc94418..51a49948fe 100644 --- a/osu.Game/Database/IHasPrimaryKey.cs +++ b/osu.Game/Database/IHasPrimaryKey.cs @@ -11,5 +11,7 @@ namespace osu.Game.Database [JsonIgnore] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] int ID { get; set; } + + bool IsManaged { get; } } } diff --git a/osu.Game/IO/FileInfo.cs b/osu.Game/IO/FileInfo.cs index 331546f9f8..360f8440f1 100644 --- a/osu.Game/IO/FileInfo.cs +++ b/osu.Game/IO/FileInfo.cs @@ -10,6 +10,8 @@ namespace osu.Game.IO { public int ID { get; set; } + public bool IsManaged => ID > 0; + public string Hash { get; set; } public string StoragePath => Path.Combine(Hash.Remove(1), Hash.Remove(2), Hash); diff --git a/osu.Game/Scoring/ScoreFileInfo.cs b/osu.Game/Scoring/ScoreFileInfo.cs index 9075fdec5b..d98ef9fdc6 100644 --- a/osu.Game/Scoring/ScoreFileInfo.cs +++ b/osu.Game/Scoring/ScoreFileInfo.cs @@ -11,6 +11,8 @@ namespace osu.Game.Scoring { public int ID { get; set; } + public bool IsManaged => ID > 0; + public int FileInfoID { get; set; } public FileInfo FileInfo { get; set; } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 736a939a59..2b02dfef9f 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -22,6 +22,8 @@ namespace osu.Game.Scoring { public int ID { get; set; } + public bool IsManaged => ID > 0; + public ScoreRank Rank { get; set; } public long TotalScore { get; set; } diff --git a/osu.Game/Skinning/SkinFileInfo.cs b/osu.Game/Skinning/SkinFileInfo.cs index 8a7019e1a3..06d0d5e82e 100644 --- a/osu.Game/Skinning/SkinFileInfo.cs +++ b/osu.Game/Skinning/SkinFileInfo.cs @@ -11,6 +11,8 @@ namespace osu.Game.Skinning { public int ID { get; set; } + public bool IsManaged => ID > 0; + public int SkinInfoID { get; set; } public int FileInfoID { get; set; } diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index 3b34e23d57..307bb2f9cd 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -18,6 +18,8 @@ namespace osu.Game.Skinning public int ID { get; set; } + public bool IsManaged => ID > 0; + public string Name { get; set; } = string.Empty; public string Creator { get; set; } = string.Empty; From 83b4625bd54cff98c758e82daf4fd8fc5ec27e46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Nov 2021 22:13:07 +0900 Subject: [PATCH 002/419] Replace existing cases with new helper method --- .../Visual/Editing/TestSceneEditorBeatmapCreation.cs | 2 +- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 2 +- osu.Game/Database/ArchiveModelManager.cs | 6 +++--- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 4 ++-- osu.Game/Screens/Select/SongSelect.cs | 4 ++-- osu.Game/Skinning/SkinManager.cs | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 92c8131568..db20d3c7ba 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Editing public void TestCreateNewBeatmap() { AddStep("save beatmap", () => Editor.Save()); - AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0); + AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.IsManaged); AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == false); } diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 5f20bf49b9..17d64bce4d 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -131,7 +131,7 @@ namespace osu.Game.Beatmaps var localRulesetInfo = rulesetInfo as RulesetInfo; // Difficulty can only be computed if the beatmap and ruleset are locally available. - if (localBeatmapInfo == null || localBeatmapInfo.ID == 0 || localRulesetInfo == null) + if (localBeatmapInfo?.IsManaged != true || localRulesetInfo == null) { // If not, fall back to the existing star difficulty (e.g. from an online source). return Task.FromResult(new StarDifficulty(beatmapInfo.StarRating, (beatmapInfo as IBeatmapOnlineInfo)?.MaxCombo ?? 0)); diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index e8b6996869..5f9304448a 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -516,7 +516,7 @@ namespace osu.Game.Database { Files.Dereference(file.FileInfo); - if (file.ID > 0) + if (file.IsManaged) { // This shouldn't be required, but here for safety in case the provided TModel is not being change tracked // Definitely can be removed once we rework the database backend. @@ -545,7 +545,7 @@ namespace osu.Game.Database }); } - if (model.ID > 0) + if (model.IsManaged) Update(model); } @@ -811,7 +811,7 @@ namespace osu.Game.Database /// The usable items present in the store. /// Whether the exists. protected virtual bool CheckLocalAvailability(TModel model, IQueryable items) - => model.ID > 0 && items.Any(i => i.ID == model.ID && i.Files.Any()); + => model.IsManaged && items.Any(i => i.ID == model.ID && i.Files.Any()); /// /// Whether import can be skipped after finding an existing import early in the process. diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 0714b28b47..e17abb5936 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -45,7 +45,7 @@ namespace osu.Game.Overlays.Settings.Sections { get { - int index = skinItems.FindIndex(s => s.ID > 0); + int index = skinItems.FindIndex(s => s.IsManaged); if (index < 0) index = skinItems.Count; @@ -176,7 +176,7 @@ namespace osu.Game.Overlays.Settings.Sections Action = export; currentSkin = skins.CurrentSkin.GetBoundCopy(); - currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.ID > 0, true); + currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.IsManaged, true); } private void export() diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 2c36bf5fc8..969a6e0290 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -805,14 +805,14 @@ namespace osu.Game.Screens.Select private void delete(BeatmapSetInfo beatmap) { - if (beatmap == null || beatmap.ID <= 0) return; + if (beatmap == null || !beatmap.IsManaged) return; dialogOverlay?.Push(new BeatmapDeleteDialog(beatmap)); } private void clearScores(BeatmapInfo beatmapInfo) { - if (beatmapInfo == null || beatmapInfo.ID <= 0) return; + if (beatmapInfo == null || !beatmapInfo.IsManaged) return; dialogOverlay?.Push(new BeatmapClearScoresDialog(beatmapInfo, () => // schedule done here rather than inside the dialog as the dialog may fade out and never callback. diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 0739026544..2887d03029 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -307,7 +307,7 @@ namespace osu.Game.Skinning public void Save(Skin skin) { - if (skin.SkinInfo.ID <= 0) + if (!skin.SkinInfo.IsManaged) throw new InvalidOperationException($"Attempting to save a skin which is not yet tracked. Call {nameof(EnsureMutableSkin)} first."); foreach (var drawableInfo in skin.DrawableComponentInfo) From 7560d3de0435408c62d7e8ad9fbedfcb1e211747 Mon Sep 17 00:00:00 2001 From: MBmasher Date: Mon, 22 Nov 2021 10:52:04 +1100 Subject: [PATCH 003/419] Remove decay factor in Flashlight skill --- osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 466f0556ab..68434fd3d0 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { } - private double skillMultiplier => 0.15; + private double skillMultiplier => 0.07; private double strainDecayBase => 0.15; protected override double DecayWeight => 1.0; protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations. @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills // We also want to nerf stacks so that only the first object of the stack is accounted for. double stackNerf = Math.Min(1.0, (osuPrevious.JumpDistance / scalingFactor) / 25.0); - result += Math.Pow(0.8, i) * stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime; + result += stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime; } } From 0633f3bcfe23513d671c0d5dc7bf7efcb603ba44 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 22 Nov 2021 16:35:58 +0900 Subject: [PATCH 004/419] Add owner id to playlist items --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 1 + osu.Game/Online/Rooms/PlaylistItem.cs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index ad3c1f6781..df16fb3042 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -720,6 +720,7 @@ namespace osu.Game.Online.Multiplayer var playlistItem = new PlaylistItem { ID = item.ID, + OwnerID = item.OwnerID, Beatmap = { Value = beatmap }, Ruleset = { Value = ruleset }, Expired = item.Expired diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index c889dc514b..a1480865b8 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -18,6 +18,9 @@ namespace osu.Game.Online.Rooms [JsonProperty("id")] public long ID { get; set; } + [JsonProperty("owner_id")] + public int OwnerID { get; set; } + [JsonProperty("beatmap_id")] public int BeatmapID { get; set; } From 9157b91e5f2aebb4fcbee2052227a27c781041fa Mon Sep 17 00:00:00 2001 From: GoldenMine0502 Date: Tue, 23 Nov 2021 16:41:20 +0900 Subject: [PATCH 005/419] fix adding wrong values --- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 24881d9c47..4f87767fa7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (strainTime < min_speed_bonus) speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); - double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.JumpDistance); + double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.MovementDistance); return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime; } From e67d9b1c2153c068a762820b866372818a2c5405 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Nov 2021 12:14:52 +0900 Subject: [PATCH 006/419] Reorder members a bit --- .../Preprocessing/OsuDifficultyHitObject.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index d073d751d0..5c163eeb76 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -18,6 +18,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject; + /// + /// Milliseconds elapsed since the start time of the previous , with a minimum of 25ms. + /// + public readonly double StrainTime; + /// /// Normalized distance from the end position of the previous to the start position of this . /// @@ -28,31 +33,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing /// public double MovementDistance { get; private set; } - /// - /// Normalized distance between the start and end position of the previous . - /// - public double TravelDistance { get; private set; } - - /// - /// Angle the player has to take to hit this . - /// Calculated as the angle between the circles (current-2, current-1, current). - /// - public double? Angle { get; private set; } - /// /// Milliseconds elapsed since the end time of the previous , with a minimum of 25ms. /// public double MovementTime { get; private set; } + /// + /// Normalized distance between the start and end position of the previous . + /// + public double TravelDistance { get; private set; } + /// /// Milliseconds elapsed since the start time of the previous to the end time of the same previous , with a minimum of 25ms. /// public double TravelTime { get; private set; } /// - /// Milliseconds elapsed since the start time of the previous , with a minimum of 25ms. + /// Angle the player has to take to hit this . + /// Calculated as the angle between the circles (current-2, current-1, current). /// - public readonly double StrainTime; + public double? Angle { get; private set; } private readonly OsuHitObject lastLastObject; private readonly OsuHitObject lastObject; From 402de754f7f459fc48fd0d86a88c5f708a03a2c9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Nov 2021 12:37:16 +0900 Subject: [PATCH 007/419] Make TravelDistance/TravelTime apply to the current object --- .../Preprocessing/OsuDifficultyHitObject.cs | 27 ++++++++++--------- .../Difficulty/Skills/Aim.cs | 12 ++++----- .../Difficulty/Skills/Speed.cs | 3 ++- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 5c163eeb76..eb36e96995 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -39,12 +39,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing public double MovementTime { get; private set; } /// - /// Normalized distance between the start and end position of the previous . + /// Normalized distance between the start and end position of this . /// public double TravelDistance { get; private set; } /// - /// Milliseconds elapsed since the start time of the previous to the end time of the same previous , with a minimum of 25ms. + /// Milliseconds elapsed between the start and end time of this , with a minimum of 25ms. /// public double TravelTime { get; private set; } @@ -84,15 +84,23 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing scalingFactor *= 1 + smallCircleBonus; } + if (BaseObject is Slider currentSlider) + { + computeSliderCursorPosition(currentSlider); + TravelDistance = currentSlider.LazyTravelDistance; + TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time); + } + Vector2 lastCursorPosition = getEndCursorPosition(lastObject); + JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length; + MovementTime = StrainTime; + MovementDistance = JumpDistance; if (lastObject is Slider lastSlider) { - computeSliderCursorPosition(lastSlider); - TravelDistance = lastSlider.LazyTravelDistance; - TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time); - MovementTime = Math.Max(StrainTime - TravelTime, min_delta_time); + double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time); + MovementTime = Math.Max(MovementTime - lastTravelTime, min_delta_time); // Jump distance from the slider tail to the next object, as opposed to the lazy position of JumpDistance. float tailJumpDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor; @@ -102,12 +110,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing // In such cases, a leniency is applied by also considering the jump distance from the tail of the slider, and taking the minimum jump distance. // Additional distance is removed based on position of jump relative to slider follow circle radius. // JumpDistance is the leniency distance beyond the assumed_slider_radius. tailJumpDistance is maximum_slider_radius since the full distance of radial leniency is still possible. - MovementDistance = Math.Max(0, Math.Min(JumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius)); - } - else - { - MovementTime = StrainTime; - MovementDistance = JumpDistance; + MovementDistance = Math.Max(0, Math.Min(MovementDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius)); } if (lastLastObject != null && !(lastLastObject is Spinner)) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 2a8d2ce759..4ddcbb7770 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (osuLastObj.BaseObject is Slider && withSliders) { double movementVelocity = osuCurrObj.MovementDistance / osuCurrObj.MovementTime; // calculate the movement velocity from slider end to current object - double travelVelocity = osuCurrObj.TravelDistance / osuCurrObj.TravelTime; // calculate the slider velocity from slider head to slider end. + double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; // calculate the slider velocity from slider head to slider end. currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity. } @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (osuLastLastObj.BaseObject is Slider && withSliders) { double movementVelocity = osuLastObj.MovementDistance / osuLastObj.MovementTime; - double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; + double travelVelocity = osuLastLastObj.TravelDistance / osuLastLastObj.TravelTime; prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity); } @@ -107,8 +107,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (Math.Max(prevVelocity, currVelocity) != 0) { // We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities. - prevVelocity = (osuLastObj.JumpDistance + osuLastObj.TravelDistance) / osuLastObj.StrainTime; - currVelocity = (osuCurrObj.JumpDistance + osuCurrObj.TravelDistance) / osuCurrObj.StrainTime; + prevVelocity = (osuLastObj.JumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.StrainTime; + currVelocity = (osuCurrObj.JumpDistance + osuLastObj.TravelDistance) / osuCurrObj.StrainTime; // Scale with ratio of difference compared to 0.5 * max dist. double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2); @@ -128,10 +128,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2); } - if (osuCurrObj.TravelTime != 0) + if (osuLastObj.TravelTime != 0) { // Reward sliders based on velocity. - sliderBonus = osuCurrObj.TravelDistance / osuCurrObj.TravelTime; + sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime; } // Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger. diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 24881d9c47..b53d287ee6 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -154,7 +154,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (strainTime < min_speed_bonus) speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); - double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.JumpDistance); + double travelDistance = osuPrevObj?.TravelDistance ?? 0; + double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.JumpDistance); return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime; } From e07c44d79a02028f0d944b51e86a053b3940e307 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Nov 2021 13:01:15 +0900 Subject: [PATCH 008/419] Reword comment with a more diagrammatical explanation --- .../Preprocessing/OsuDifficultyHitObject.cs | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index eb36e96995..ecba2500d0 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -102,14 +102,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time); MovementTime = Math.Max(MovementTime - lastTravelTime, min_delta_time); - // Jump distance from the slider tail to the next object, as opposed to the lazy position of JumpDistance. - float tailJumpDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor; + // + // We'll try to better approximate the real movements a player will take in patterns following on from sliders. Consider the following slider-to-object patterns: + // + // 1. <======o==> + // | / + // o + // + // 2. <======o==>---o + // |______| + // + // Where "<==>" represents a slider, and "o" represents where the cursor needs to be for either hitobject (for a slider, this is the lazy cursor position). + // + // Case (1) is an anti-flow pattern, where players will cut the slider short in order to move to the next object. The jump pattern is (o--o). + // Case (2) is a flow pattern, where players will follow the slider through to its visual extent. The jump pattern is (>--o). + // + // A lenience is applied by assuming that the player jumps the minimum of these two distances in all cases. + // - // For hitobjects which continue in the direction of the slider, the player will normally follow through the slider, - // such that they're not jumping from the lazy position but rather from very close to (or the end of) the slider. - // In such cases, a leniency is applied by also considering the jump distance from the tail of the slider, and taking the minimum jump distance. - // Additional distance is removed based on position of jump relative to slider follow circle radius. - // JumpDistance is the leniency distance beyond the assumed_slider_radius. tailJumpDistance is maximum_slider_radius since the full distance of radial leniency is still possible. + float tailJumpDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor; MovementDistance = Math.Max(0, Math.Min(MovementDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius)); } From a081038076f647b6fa7632135f71766b8a97514c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Nov 2021 13:01:53 +0900 Subject: [PATCH 009/419] Normalized -> Normalised --- .../Preprocessing/OsuDifficultyHitObject.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index ecba2500d0..5c20851e23 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -11,10 +11,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing { public class OsuDifficultyHitObject : DifficultyHitObject { - private const int normalized_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. + private const int normalised_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. private const int min_delta_time = 25; - private const float maximum_slider_radius = normalized_radius * 2.4f; - private const float assumed_slider_radius = normalized_radius * 1.8f; + private const float maximum_slider_radius = normalised_radius * 2.4f; + private const float assumed_slider_radius = normalised_radius * 1.8f; protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject; @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing public readonly double StrainTime; /// - /// Normalized distance from the end position of the previous to the start position of this . + /// Normalised distance from the end position of the previous to the start position of this . /// public double JumpDistance { get; private set; } @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing public double MovementTime { get; private set; } /// - /// Normalized distance between the start and end position of this . + /// Normalised distance between the start and end position of this . /// public double TravelDistance { get; private set; } @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing return; // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps. - float scalingFactor = normalized_radius / (float)BaseObject.Radius; + float scalingFactor = normalised_radius / (float)BaseObject.Radius; if (BaseObject.Radius < 30) { @@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived. var currCursorPosition = slider.StackedPosition; - double scalingFactor = normalized_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used. + double scalingFactor = normalised_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used. for (int i = 1; i < slider.NestedHitObjects.Count; i++) { @@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing else if (currMovementObj is SliderRepeat) { // For a slider repeat, assume a tighter movement threshold to better assess repeat sliders. - requiredMovement = normalized_radius; + requiredMovement = normalised_radius; } if (currMovementLength > requiredMovement) From b5747f351dac23d33f63b3b804ea1052a7c5a5de Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Nov 2021 13:11:44 +0900 Subject: [PATCH 010/419] Reword xmldocs --- .../Difficulty/Preprocessing/OsuDifficultyHitObject.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 5c20851e23..52108f85f9 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -29,12 +29,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing public double JumpDistance { get; private set; } /// - /// Minimum distance from the end position of the previous to the start position of this . + /// Normalised minimum distance from the end position of the previous to the start position of this . /// + /// + /// This is bounded by , but may be smaller if a more natural path is able to be taken through a preceding slider. + /// public double MovementDistance { get; private set; } /// - /// Milliseconds elapsed since the end time of the previous , with a minimum of 25ms. + /// The time taken to travel through , with a minimum value of 25ms. /// public double MovementTime { get; private set; } @@ -44,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing public double TravelDistance { get; private set; } /// - /// Milliseconds elapsed between the start and end time of this , with a minimum of 25ms. + /// The time taken to travel through , with a minimum value of 25ms for a non-zero distance. /// public double TravelTime { get; private set; } From 274444ed6725345e3b4f0588fc046d1bba95a79f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Nov 2021 13:22:52 +0900 Subject: [PATCH 011/419] Add additional information to diagram --- .../Preprocessing/OsuDifficultyHitObject.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 52108f85f9..36a1053317 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing if (lastObject is Slider lastSlider) { double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time); - MovementTime = Math.Max(MovementTime - lastTravelTime, min_delta_time); + MovementTime = Math.Max(StrainTime - lastTravelTime, min_delta_time); // // We'll try to better approximate the real movements a player will take in patterns following on from sliders. Consider the following slider-to-object patterns: @@ -117,14 +117,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing // // Where "<==>" represents a slider, and "o" represents where the cursor needs to be for either hitobject (for a slider, this is the lazy cursor position). // - // Case (1) is an anti-flow pattern, where players will cut the slider short in order to move to the next object. The jump pattern is (o--o). - // Case (2) is a flow pattern, where players will follow the slider through to its visual extent. The jump pattern is (>--o). + // The pattern (o--o) has distance JumpDistance. + // The pattern (>--o) is a new distance we'll call "tailJumpDistance". + // + // Case (1) is an anti-flow pattern, where players will cut the slider short in order to move to the next object. The most natural jump pattern is (o--o). + // Case (2) is a flow pattern, where players will follow the slider through to its visual extent. The most natural jump pattern is (>--o). // // A lenience is applied by assuming that the player jumps the minimum of these two distances in all cases. // float tailJumpDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor; - MovementDistance = Math.Max(0, Math.Min(MovementDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius)); + MovementDistance = Math.Max(0, Math.Min(JumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius)); } if (lastLastObject != null && !(lastLastObject is Spinner)) From b20ff22af0c41294dfffedb6717e6fc78c43ec3b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Nov 2021 16:50:33 +0900 Subject: [PATCH 012/419] Ensure travel distance is calculated for all sliders --- .../Preprocessing/OsuDifficultyHitObject.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 36a1053317..f1f246359a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -74,6 +74,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing private void setDistances(double clockRate) { + if (BaseObject is Slider currentSlider) + { + computeSliderCursorPosition(currentSlider); + TravelDistance = currentSlider.LazyTravelDistance; + TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time); + } + // We don't need to calculate either angle or distance when one of the last->curr objects is a spinner if (BaseObject is Spinner || lastObject is Spinner) return; @@ -87,13 +94,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing scalingFactor *= 1 + smallCircleBonus; } - if (BaseObject is Slider currentSlider) - { - computeSliderCursorPosition(currentSlider); - TravelDistance = currentSlider.LazyTravelDistance; - TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time); - } - Vector2 lastCursorPosition = getEndCursorPosition(lastObject); JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length; From 3e4b774992b7aa811d60bc75e5b7979ba4889f07 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 25 Nov 2021 14:08:08 +0900 Subject: [PATCH 013/419] Invert lines for better chronological order --- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 4ddcbb7770..d2a1083f29 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -49,8 +49,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills // But if the last object is a slider, then we extend the travel velocity through the slider into the current object. if (osuLastObj.BaseObject is Slider && withSliders) { - double movementVelocity = osuCurrObj.MovementDistance / osuCurrObj.MovementTime; // calculate the movement velocity from slider end to current object double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; // calculate the slider velocity from slider head to slider end. + double movementVelocity = osuCurrObj.MovementDistance / osuCurrObj.MovementTime; // calculate the movement velocity from slider end to current object currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity. } @@ -60,8 +60,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (osuLastLastObj.BaseObject is Slider && withSliders) { - double movementVelocity = osuLastObj.MovementDistance / osuLastObj.MovementTime; double travelVelocity = osuLastLastObj.TravelDistance / osuLastLastObj.TravelTime; + double movementVelocity = osuLastObj.MovementDistance / osuLastObj.MovementTime; prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity); } From e5dcfc311390a22b0b1fd6ad880980e90bc292ca Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 28 Nov 2021 14:03:21 +0900 Subject: [PATCH 014/419] Use console IPC --- osu.Desktop/Program.cs | 53 +++++++++++++++++++-- osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs | 52 ++++++++++++++++++++ 2 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 898f7d5105..6dd6849d78 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -3,13 +3,22 @@ using System; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework; using osu.Framework.Development; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Legacy; using osu.Game.IPC; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; using osu.Game.Tournament; namespace osu.Desktop @@ -19,7 +28,7 @@ namespace osu.Desktop private const string base_game_name = @"osu"; [STAThread] - public static int Main(string[] args) + public static void Main(string[] args) { // Back up the cwd before DesktopGameHost changes it string cwd = Environment.CurrentDirectory; @@ -49,6 +58,34 @@ namespace osu.Desktop gameName = $"{base_game_name}-{clientID}"; break; + + case "--osu-stable-difficulty-stream": + while (true) + { + try + { + string beatmapFile = Console.ReadLine() ?? string.Empty; + int rulesetId = int.Parse(Console.ReadLine() ?? string.Empty); + LegacyMods legacyMods = (LegacyMods)int.Parse(Console.ReadLine() ?? string.Empty); + + Ruleset ruleset = rulesetId switch + { + 0 => new OsuRuleset(), + 1 => new TaikoRuleset(), + 2 => new CatchRuleset(), + 3 => new ManiaRuleset(), + _ => throw new ArgumentException("Invalid ruleset id") + }; + + Mod[] mods = ruleset.ConvertFromLegacyMods(legacyMods).ToArray(); + WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(beatmapFile, _ => ruleset); + Console.WriteLine(ruleset.CreateDifficultyCalculator(beatmap).Calculate(mods).StarRating); + } + catch + { + Console.WriteLine(0); + } + } } } @@ -69,14 +106,14 @@ namespace osu.Desktop throw new TimeoutException(@"IPC took too long to send"); } - return 0; + return; } // we want to allow multiple instances to be started when in debug. if (!DebugUtils.IsDebugBuild) { Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error); - return 0; + return; } } @@ -84,8 +121,6 @@ namespace osu.Desktop host.Run(new TournamentGame()); else host.Run(new OsuGameDesktop(args)); - - return 0; } } @@ -107,4 +142,12 @@ namespace osu.Desktop return continueExecution; } } + + // Note: Keep in osu.Desktop namespace, or update osu!stable also. + public class DifficultyCalculationMessage + { + public string BeatmapFile { get; set; } + public int RulesetId { get; set; } + public int Mods { get; set; } + } } diff --git a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs new file mode 100644 index 0000000000..8c915e2872 --- /dev/null +++ b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.IO; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics.Textures; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO; +using osu.Game.Rulesets; +using osu.Game.Skinning; + +namespace osu.Game.Beatmaps +{ + /// + /// A which can be constructed directly from a .osu file, providing an implementation for + /// . + /// + public class FlatFileWorkingBeatmap : WorkingBeatmap + { + private readonly Beatmap beatmap; + + public FlatFileWorkingBeatmap(string file, Func rulesetProvider, int? beatmapId = null) + : this(readFromFile(file), rulesetProvider, beatmapId) + { + } + + private FlatFileWorkingBeatmap(Beatmap beatmap, Func rulesetProvider, int? beatmapId = null) + : base(beatmap.BeatmapInfo, null) + { + this.beatmap = beatmap; + + beatmap.BeatmapInfo.Ruleset = rulesetProvider(beatmap.BeatmapInfo.RulesetID).RulesetInfo; + + if (beatmapId.HasValue) + beatmap.BeatmapInfo.OnlineID = beatmapId; + } + + private static Beatmap readFromFile(string filename) + { + using (var stream = File.OpenRead(filename)) + using (var reader = new LineBufferedReader(stream)) + return Decoder.GetDecoder(reader).Decode(reader); + } + + protected override IBeatmap GetBeatmap() => beatmap; + protected override Texture GetBackground() => throw new NotImplementedException(); + protected override Track GetBeatmapTrack() => throw new NotImplementedException(); + protected internal override ISkin GetSkin() => throw new NotImplementedException(); + public override Stream GetStream(string storagePath) => throw new NotImplementedException(); + } +} From ef247806429270a6e69fb0a25bcb8d81ae124ba7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 28 Nov 2021 18:00:06 +0900 Subject: [PATCH 015/419] Use IPC via TCP --- .../LegacyIpcDifficultyCalculationRequest.cs | 18 ++++ .../LegacyIpcDifficultyCalculationResponse.cs | 16 ++++ osu.Desktop/LegacyIpc/LegacyIpcMessage.cs | 40 +++++++++ osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs | 65 ++++++++++++++ osu.Desktop/Program.cs | 86 ++++++++++++------- 5 files changed, 193 insertions(+), 32 deletions(-) create mode 100644 osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationRequest.cs create mode 100644 osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationResponse.cs create mode 100644 osu.Desktop/LegacyIpc/LegacyIpcMessage.cs create mode 100644 osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs diff --git a/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationRequest.cs b/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationRequest.cs new file mode 100644 index 0000000000..d6ef390a8f --- /dev/null +++ b/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationRequest.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Desktop.LegacyIpc +{ + /// + /// A difficulty calculation request from the legacy client. + /// + /// + /// Synchronise any changes with osu!stable. + /// + public class LegacyIpcDifficultyCalculationRequest + { + public string BeatmapFile { get; set; } + public int RulesetId { get; set; } + public int Mods { get; set; } + } +} diff --git a/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationResponse.cs b/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationResponse.cs new file mode 100644 index 0000000000..7b9fae5797 --- /dev/null +++ b/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationResponse.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. + +namespace osu.Desktop.LegacyIpc +{ + /// + /// A difficulty calculation response returned to the legacy client. + /// + /// + /// Synchronise any changes with osu!stable. + /// + public class LegacyIpcDifficultyCalculationResponse + { + public double StarRating { get; set; } + } +} diff --git a/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs b/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs new file mode 100644 index 0000000000..6fefae4509 --- /dev/null +++ b/osu.Desktop/LegacyIpc/LegacyIpcMessage.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.Platform; + +namespace osu.Desktop.LegacyIpc +{ + /// + /// An that can be used to communicate to and from legacy clients. + /// + /// + /// Synchronise any changes with osu-stable. + /// + public class LegacyIpcMessage : IpcMessage + { + public LegacyIpcMessage() + { + // Types/assemblies are not inter-compatible, so always serialise/deserialise into objects. + base.Type = typeof(object).FullName; + } + + public new string Type => base.Type; // Hide setter. + + public new object Value + { + get => base.Value; + set => base.Value = new Data + { + MessageType = value.GetType().Name, + MessageData = value + }; + } + + public class Data + { + public string MessageType { get; set; } + public object MessageData { get; set; } + } + } +} diff --git a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs new file mode 100644 index 0000000000..4fca40f22f --- /dev/null +++ b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using osu.Framework.Platform; + +namespace osu.Desktop.LegacyIpc +{ + public class LegacyTcpIpcProvider : TcpIpcProvider + { + public new Func MessageReceived; + + public LegacyTcpIpcProvider() + { + base.MessageReceived += msg => + { + try + { + var legacyData = ((JObject)msg.Value).ToObject(); + object value = parseObject((JObject)legacyData.MessageData, legacyData.MessageType); + + object result = MessageReceived?.Invoke(value); + return result != null ? new LegacyIpcMessage { Value = result } : null; + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + + return null; + }; + } + + public Task SendMessageAsync(object message) => base.SendMessageAsync(new LegacyIpcMessage { Value = message }); + + public async Task SendMessageWithResponseAsync(object message) + { + var result = await base.SendMessageWithResponseAsync(new LegacyIpcMessage { Value = message }).ConfigureAwait(false); + + var legacyData = ((JObject)result.Value).ToObject(); + return (T)parseObject((JObject)legacyData.MessageData, legacyData.MessageType); + } + + public new Task SendMessageAsync(IpcMessage message) => throw new InvalidOperationException("Use typed overloads."); + + public new Task SendMessageWithResponseAsync(IpcMessage message) => throw new InvalidOperationException("Use typed overloads."); + + private object parseObject(JObject value, string type) + { + switch (type) + { + case nameof(LegacyIpcDifficultyCalculationRequest): + return value.ToObject(); + + case nameof(LegacyIpcDifficultyCalculationResponse): + return value.ToObject(); + + default: + throw new ArgumentException($"Unknown type: {type}"); + } + } + } +} diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 6dd6849d78..cb9204f518 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using osu.Desktop.LegacyIpc; using osu.Framework; using osu.Framework.Development; using osu.Framework.Logging; @@ -27,6 +28,8 @@ namespace osu.Desktop { private const string base_game_name = @"osu"; + private static LegacyTcpIpcProvider legacyIpcProvider; + [STAThread] public static void Main(string[] args) { @@ -59,33 +62,26 @@ namespace osu.Desktop gameName = $"{base_game_name}-{clientID}"; break; - case "--osu-stable-difficulty-stream": - while (true) + case "--legacy-ipc-server": + using (legacyIpcProvider = new LegacyTcpIpcProvider()) { - try - { - string beatmapFile = Console.ReadLine() ?? string.Empty; - int rulesetId = int.Parse(Console.ReadLine() ?? string.Empty); - LegacyMods legacyMods = (LegacyMods)int.Parse(Console.ReadLine() ?? string.Empty); - - Ruleset ruleset = rulesetId switch - { - 0 => new OsuRuleset(), - 1 => new TaikoRuleset(), - 2 => new CatchRuleset(), - 3 => new ManiaRuleset(), - _ => throw new ArgumentException("Invalid ruleset id") - }; - - Mod[] mods = ruleset.ConvertFromLegacyMods(legacyMods).ToArray(); - WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(beatmapFile, _ => ruleset); - Console.WriteLine(ruleset.CreateDifficultyCalculator(beatmap).Calculate(mods).StarRating); - } - catch - { - Console.WriteLine(0); - } + legacyIpcProvider.MessageReceived += onLegacyIpcMessageReceived; + legacyIpcProvider.Bind(); + legacyIpcProvider.StartAsync().Wait(); } + + return; + + case "--legacy-ipc-client": + using (legacyIpcProvider = new LegacyTcpIpcProvider()) + { + Console.WriteLine(legacyIpcProvider.SendMessageWithResponseAsync(new LegacyIpcDifficultyCalculationRequest + { + BeatmapFile = "/home/smgi/Downloads/osu_files/129891.osu", + }).Result.StarRating); + } + + return; } } @@ -141,13 +137,39 @@ namespace osu.Desktop return continueExecution; } - } - // Note: Keep in osu.Desktop namespace, or update osu!stable also. - public class DifficultyCalculationMessage - { - public string BeatmapFile { get; set; } - public int RulesetId { get; set; } - public int Mods { get; set; } + private static object onLegacyIpcMessageReceived(object message) + { + switch (message) + { + case LegacyIpcDifficultyCalculationRequest req: + try + { + Ruleset ruleset = req.RulesetId switch + { + 0 => new OsuRuleset(), + 1 => new TaikoRuleset(), + 2 => new CatchRuleset(), + 3 => new ManiaRuleset(), + _ => throw new ArgumentException("Invalid ruleset id") + }; + + Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray(); + WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile, _ => ruleset); + + return new LegacyIpcDifficultyCalculationResponse + { + StarRating = ruleset.CreateDifficultyCalculator(beatmap).Calculate(mods).StarRating + }; + } + catch + { + return new LegacyIpcDifficultyCalculationResponse(); + } + } + + Console.WriteLine("Type not matched."); + return null; + } } } From 5711c428caa9af871aa2e8e2509f16896470eabb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 28 Nov 2021 21:15:21 +0900 Subject: [PATCH 016/419] Increment IPC port --- osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs index 4fca40f22f..5d5a34e850 100644 --- a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs +++ b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs @@ -13,6 +13,7 @@ namespace osu.Desktop.LegacyIpc public new Func MessageReceived; public LegacyTcpIpcProvider() + : base(45357) { base.MessageReceived += msg => { From f506cb35bc4781b404b0f039f5978a11514d80f8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 28 Nov 2021 21:15:29 +0900 Subject: [PATCH 017/419] Bind legacy IPC on startup --- osu.Desktop/Program.cs | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index cb9204f518..7762aa9010 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -61,27 +61,6 @@ namespace osu.Desktop gameName = $"{base_game_name}-{clientID}"; break; - - case "--legacy-ipc-server": - using (legacyIpcProvider = new LegacyTcpIpcProvider()) - { - legacyIpcProvider.MessageReceived += onLegacyIpcMessageReceived; - legacyIpcProvider.Bind(); - legacyIpcProvider.StartAsync().Wait(); - } - - return; - - case "--legacy-ipc-client": - using (legacyIpcProvider = new LegacyTcpIpcProvider()) - { - Console.WriteLine(legacyIpcProvider.SendMessageWithResponseAsync(new LegacyIpcDifficultyCalculationRequest - { - BeatmapFile = "/home/smgi/Downloads/osu_files/129891.osu", - }).Result.StarRating); - } - - return; } } @@ -113,6 +92,14 @@ namespace osu.Desktop } } + if (host.IsPrimaryInstance) + { + var legacyIpc = new LegacyTcpIpcProvider(); + legacyIpc.MessageReceived += onLegacyIpcMessageReceived; + legacyIpc.Bind(); + legacyIpc.StartAsync(); + } + if (tournamentClient) host.Run(new TournamentGame()); else From 36fffbd9178585aa3c497918a220f9df767c75b6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 28 Nov 2021 21:31:22 +0900 Subject: [PATCH 018/419] Refactoring --- osu.Desktop/LegacyIpc/LegacyIpcMessage.cs | 1 - osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs | 33 ++++++++----------- osu.Desktop/Program.cs | 2 -- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs b/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs index 6fefae4509..2e421068dc 100644 --- a/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs +++ b/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs @@ -23,7 +23,6 @@ namespace osu.Desktop.LegacyIpc public new object Value { - get => base.Value; set => base.Value = new Data { MessageType = value.GetType().Name, diff --git a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs index 5d5a34e850..43d9dd741c 100644 --- a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs +++ b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs @@ -2,14 +2,22 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Threading.Tasks; using Newtonsoft.Json.Linq; +using osu.Framework.Logging; using osu.Framework.Platform; namespace osu.Desktop.LegacyIpc { + /// + /// Provides IPC to legacy osu! clients. + /// public class LegacyTcpIpcProvider : TcpIpcProvider { + private static readonly Logger logger = Logger.GetLogger("ipc"); + + /// + /// Invoked when a message is received from a legacy client. + /// public new Func MessageReceived; public LegacyTcpIpcProvider() @@ -19,35 +27,22 @@ namespace osu.Desktop.LegacyIpc { try { + logger.Add($"Processing incoming IPC message: {msg.Value}"); + var legacyData = ((JObject)msg.Value).ToObject(); - object value = parseObject((JObject)legacyData.MessageData, legacyData.MessageType); + object value = parseObject((JObject)legacyData!.MessageData, legacyData.MessageType); object result = MessageReceived?.Invoke(value); return result != null ? new LegacyIpcMessage { Value = result } : null; } catch (Exception ex) { - Console.WriteLine(ex); + logger.Add("Processing IPC message failed!", exception: ex); + return null; } - - return null; }; } - public Task SendMessageAsync(object message) => base.SendMessageAsync(new LegacyIpcMessage { Value = message }); - - public async Task SendMessageWithResponseAsync(object message) - { - var result = await base.SendMessageWithResponseAsync(new LegacyIpcMessage { Value = message }).ConfigureAwait(false); - - var legacyData = ((JObject)result.Value).ToObject(); - return (T)parseObject((JObject)legacyData.MessageData, legacyData.MessageType); - } - - public new Task SendMessageAsync(IpcMessage message) => throw new InvalidOperationException("Use typed overloads."); - - public new Task SendMessageWithResponseAsync(IpcMessage message) => throw new InvalidOperationException("Use typed overloads."); - private object parseObject(JObject value, string type) { switch (type) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 7762aa9010..c41a6a4f5d 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -28,8 +28,6 @@ namespace osu.Desktop { private const string base_game_name = @"osu"; - private static LegacyTcpIpcProvider legacyIpcProvider; - [STAThread] public static void Main(string[] args) { From 27ba3c6d1a8c18bb69aeac5e38d97966dcba56ed Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 28 Nov 2021 22:16:21 +0900 Subject: [PATCH 019/419] Add back removed getter Seems to somehow be required. --- osu.Desktop/LegacyIpc/LegacyIpcMessage.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs b/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs index 2e421068dc..6fefae4509 100644 --- a/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs +++ b/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs @@ -23,6 +23,7 @@ namespace osu.Desktop.LegacyIpc public new object Value { + get => base.Value; set => base.Value = new Data { MessageType = value.GetType().Name, From 18a0a791fd9f56c08a81b622ea0f196fef00097a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 28 Nov 2021 22:24:42 +0900 Subject: [PATCH 020/419] Refactor --- osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs | 5 +++-- osu.Desktop/Program.cs | 18 ++++++++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs index 43d9dd741c..7855f9c7ce 100644 --- a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs +++ b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs @@ -13,7 +13,7 @@ namespace osu.Desktop.LegacyIpc /// public class LegacyTcpIpcProvider : TcpIpcProvider { - private static readonly Logger logger = Logger.GetLogger("ipc"); + private static readonly Logger logger = Logger.GetLogger("legacy-ipc"); /// /// Invoked when a message is received from a legacy client. @@ -27,7 +27,8 @@ namespace osu.Desktop.LegacyIpc { try { - logger.Add($"Processing incoming IPC message: {msg.Value}"); + logger.Add($"Processing legacy IPC message..."); + logger.Add($"\t{msg.Value}", LogLevel.Debug); var legacyData = ((JObject)msg.Value).ToObject(); object value = parseObject((JObject)legacyData!.MessageData, legacyData.MessageType); diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index c41a6a4f5d..9542ccd8dc 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -28,6 +28,8 @@ namespace osu.Desktop { private const string base_game_name = @"osu"; + private static LegacyTcpIpcProvider legacyIpc; + [STAThread] public static void Main(string[] args) { @@ -92,10 +94,18 @@ namespace osu.Desktop if (host.IsPrimaryInstance) { - var legacyIpc = new LegacyTcpIpcProvider(); - legacyIpc.MessageReceived += onLegacyIpcMessageReceived; - legacyIpc.Bind(); - legacyIpc.StartAsync(); + try + { + Logger.Log("Starting legacy IPC provider..."); + legacyIpc = new LegacyTcpIpcProvider(); + legacyIpc.MessageReceived += onLegacyIpcMessageReceived; + legacyIpc.Bind(); + legacyIpc.StartAsync(); + } + catch (Exception ex) + { + Logger.Error(ex, "Failed to start legacy IPC provider"); + } } if (tournamentClient) From fc3eb08452d620fb0b5c5158be30313f117c1ca5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 28 Nov 2021 22:27:59 +0900 Subject: [PATCH 021/419] Output raw message on failure --- osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs index 7855f9c7ce..4fb500fa9c 100644 --- a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs +++ b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs @@ -38,7 +38,7 @@ namespace osu.Desktop.LegacyIpc } catch (Exception ex) { - logger.Add("Processing IPC message failed!", exception: ex); + logger.Add($"Processing IPC message failed: {msg.Value}", exception: ex); return null; } }; From 0d147b4ad9f7a7bc308eef9508034b45b499add5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 28 Nov 2021 22:59:49 +0900 Subject: [PATCH 022/419] Return null IPC response for archive imports --- osu.Game/IPC/ArchiveImportIPCChannel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/IPC/ArchiveImportIPCChannel.cs b/osu.Game/IPC/ArchiveImportIPCChannel.cs index d9d0e4c0ea..f381aad39a 100644 --- a/osu.Game/IPC/ArchiveImportIPCChannel.cs +++ b/osu.Game/IPC/ArchiveImportIPCChannel.cs @@ -18,6 +18,7 @@ namespace osu.Game.IPC : base(host) { this.importer = importer; + MessageReceived += msg => { Debug.Assert(importer != null); @@ -25,6 +26,8 @@ namespace osu.Game.IPC { if (t.Exception != null) throw t.Exception; }, TaskContinuationOptions.OnlyOnFaulted); + + return null; }; } From 4ee2063683f7fb14a9e93a0f36e1800e6f20b514 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 28 Nov 2021 23:02:57 +0900 Subject: [PATCH 023/419] Move event handlign internal to LegacyTcpIpcProvider --- osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs | 45 ++++++++++++++++++- osu.Desktop/Program.cs | 44 ------------------ 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs index 4fb500fa9c..273e17d24b 100644 --- a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs +++ b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs @@ -2,9 +2,18 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using Newtonsoft.Json.Linq; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; namespace osu.Desktop.LegacyIpc { @@ -33,7 +42,7 @@ namespace osu.Desktop.LegacyIpc var legacyData = ((JObject)msg.Value).ToObject(); object value = parseObject((JObject)legacyData!.MessageData, legacyData.MessageType); - object result = MessageReceived?.Invoke(value); + object result = onLegacyIpcMessageReceived(value); return result != null ? new LegacyIpcMessage { Value = result } : null; } catch (Exception ex) @@ -58,5 +67,39 @@ namespace osu.Desktop.LegacyIpc throw new ArgumentException($"Unknown type: {type}"); } } + + private object onLegacyIpcMessageReceived(object message) + { + switch (message) + { + case LegacyIpcDifficultyCalculationRequest req: + try + { + Ruleset ruleset = req.RulesetId switch + { + 0 => new OsuRuleset(), + 1 => new TaikoRuleset(), + 2 => new CatchRuleset(), + 3 => new ManiaRuleset(), + _ => throw new ArgumentException("Invalid ruleset id") + }; + + Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray(); + WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile, _ => ruleset); + + return new LegacyIpcDifficultyCalculationResponse + { + StarRating = ruleset.CreateDifficultyCalculator(beatmap).Calculate(mods).StarRating + }; + } + catch + { + return new LegacyIpcDifficultyCalculationResponse(); + } + } + + Console.WriteLine("Type not matched."); + return null; + } } } diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 9542ccd8dc..a9e3575a49 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -3,7 +3,6 @@ using System; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Desktop.LegacyIpc; @@ -11,15 +10,7 @@ using osu.Framework; using osu.Framework.Development; using osu.Framework.Logging; using osu.Framework.Platform; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Legacy; using osu.Game.IPC; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Catch; -using osu.Game.Rulesets.Mania; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Taiko; using osu.Game.Tournament; namespace osu.Desktop @@ -98,7 +89,6 @@ namespace osu.Desktop { Logger.Log("Starting legacy IPC provider..."); legacyIpc = new LegacyTcpIpcProvider(); - legacyIpc.MessageReceived += onLegacyIpcMessageReceived; legacyIpc.Bind(); legacyIpc.StartAsync(); } @@ -132,39 +122,5 @@ namespace osu.Desktop return continueExecution; } - - private static object onLegacyIpcMessageReceived(object message) - { - switch (message) - { - case LegacyIpcDifficultyCalculationRequest req: - try - { - Ruleset ruleset = req.RulesetId switch - { - 0 => new OsuRuleset(), - 1 => new TaikoRuleset(), - 2 => new CatchRuleset(), - 3 => new ManiaRuleset(), - _ => throw new ArgumentException("Invalid ruleset id") - }; - - Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray(); - WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile, _ => ruleset); - - return new LegacyIpcDifficultyCalculationResponse - { - StarRating = ruleset.CreateDifficultyCalculator(beatmap).Calculate(mods).StarRating - }; - } - catch - { - return new LegacyIpcDifficultyCalculationResponse(); - } - } - - Console.WriteLine("Type not matched."); - return null; - } } } From e40e5096ea7a7d71e4070aa9e1e095b5b4cc1f7b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Nov 2021 14:43:35 +0900 Subject: [PATCH 024/419] Remove `RealmLive` context re-fetch optimisation for now --- osu.Game/Database/RealmLive.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 73e6715aaa..dae4197309 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -114,7 +114,10 @@ namespace osu.Game.Database } } - private bool originalDataValid => !IsManaged || (isCorrectThread && data.IsValid); + // TODO: Revisit adding these conditionals back as an optimisation: || (isCorrectThread && data.IsValid); + // They have temporarily been removed due to an oversight involving .AsQueryable, see https://github.com/realm/realm-dotnet/discussions/2734. + // This means we are fetching a new context every `PerformRead` or `PerformWrite`, even when on the correct thread. + private bool originalDataValid => !IsManaged; // this matches realm's internal thread validation (see https://github.com/realm/realm-dotnet/blob/903b4d0b304f887e37e2d905384fb572a6496e70/Realm/Realm/Native/SynchronizationContextScheduler.cs#L72) private bool isCorrectThread From cb8fa803524d951d94dbb406322de050b304f9be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Nov 2021 21:33:11 +0900 Subject: [PATCH 025/419] Don't dispose fetched realm instance when using `RealmLive.Value` See https://github.com/realm/realm-dotnet/discussions/2734#discussioncomment-1705038 for reasoning. --- osu.Game/Database/RealmLive.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index dae4197309..7ae7d8544a 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -102,10 +102,11 @@ namespace osu.Game.Database if (originalDataValid) return data; - T retrieved; + if (!isCorrectThread) + throw new InvalidOperationException($"Can't use {nameof(Value)} unless on the same thread the original data was fetched from."); - using (var realm = Realm.GetInstance(data.Realm.Config)) - retrieved = realm.Find(ID); + var realm = Realm.GetInstance(data.Realm.Config); + var retrieved = realm.Find(ID); if (!retrieved.IsValid) throw new InvalidOperationException("Attempted to access value without an open context"); From 791f7e380139b40389cde6d8927e4683dc25607e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 15:14:27 +0900 Subject: [PATCH 026/419] Update `RealmLive` tests in line with modified behaviour --- osu.Game.Tests/Database/RealmLiveTests.cs | 68 ++++++++++++++--------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index f86761fdc8..63566394d6 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -66,19 +66,29 @@ namespace osu.Game.Tests.Database Task.Factory.StartNew(() => { - Assert.DoesNotThrow(() => + // TODO: The commented code is the behaviour we hope to obtain, but is temporarily disabled. + // See https://github.com/ppy/osu/pull/15851 + using (realmFactory.CreateContext()) { - using (realmFactory.CreateContext()) + Assert.Throws(() => { - var resolved = liveBeatmap.Value; + var __ = liveBeatmap.Value; + }); + } - Assert.IsTrue(resolved.Realm.IsClosed); - Assert.IsTrue(resolved.IsValid); - - // can access properties without a crash. - Assert.IsFalse(resolved.Hidden); - } - }); + // Assert.DoesNotThrow(() => + // { + // using (realmFactory.CreateContext()) + // { + // var resolved = liveBeatmap.Value; + // + // Assert.IsTrue(resolved.Realm.IsClosed); + // Assert.IsTrue(resolved.IsValid); + // + // // can access properties without a crash. + // Assert.IsFalse(resolved.Hidden); + // } + // }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); }); } @@ -199,23 +209,29 @@ namespace osu.Game.Tests.Database Assert.AreEqual(0, updateThreadContext.All().Count()); Assert.AreEqual(0, changesTriggered); - var resolved = liveBeatmap.Value; - - // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point. - Assert.AreEqual(2, updateThreadContext.All().Count()); - Assert.AreEqual(1, changesTriggered); - - // even though the realm that this instance was resolved for was closed, it's still valid. - Assert.IsTrue(resolved.Realm.IsClosed); - Assert.IsTrue(resolved.IsValid); - - // can access properties without a crash. - Assert.IsFalse(resolved.Hidden); - - updateThreadContext.Write(r => + // TODO: Originally the following was using `liveBeatmap.Value`. This has been temporarily disabled. + // See https://github.com/ppy/osu/pull/15851 + liveBeatmap.PerformRead(resolved => { - // can use with the main context. - r.Remove(resolved); + // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point. + // ReSharper disable once AccessToDisposedClosure + Assert.AreEqual(2, updateThreadContext.All().Count()); + Assert.AreEqual(1, changesTriggered); + + // TODO: as above, temporarily disabled as it doesn't make sense inside a `PerformRead`. + // // even though the realm that this instance was resolved for was closed, it's still valid. + // Assert.IsTrue(resolved.Realm.IsClosed); + // Assert.IsTrue(resolved.IsValid); + + // can access properties without a crash. + Assert.IsFalse(resolved.Hidden); + + // ReSharper disable once AccessToDisposedClosure + updateThreadContext.Write(r => + { + // can use with the main context. + r.Remove(resolved); + }); }); } From 54798eabc9c34f83466bd42296236e4cec26c531 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 16:27:16 +0900 Subject: [PATCH 027/419] Add test coverage of potential deeadlock scenario with nested realm context fetching --- osu.Game.Tests/Database/GeneralUsageTests.cs | 34 ++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index 3e8b6091fd..fec1a0dedd 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -5,6 +5,9 @@ using System; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; +using osu.Game.Database; +using osu.Game.Models; +using Realms; #nullable enable @@ -33,6 +36,37 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestNestedContextCreation() + { + RunTestWithRealm((realmFactory, _) => + { + var mainContext = realmFactory.Context; + bool callbackRan = false; + + var subscription = mainContext.All().SubscribeForNotifications((sender, changes, error) => + { + realmFactory.CreateContext(); + callbackRan = true; + }); + + Task.Factory.StartNew(() => + { + using (var threadContext = realmFactory.CreateContext()) + { + threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + + // will create a context but also run the callback above (Refresh is implicitly run when getting a new context). + realmFactory.CreateContext(); + + Assert.IsTrue(callbackRan); + + subscription.Dispose(); + }); + } + [Test] public void TestBlockOperationsWithContention() { From c98451a7bf2a75af6dab4ed82818c80f72c994c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 16:18:57 +0900 Subject: [PATCH 028/419] Fix potential deadlock on nested context creation requests --- osu.Game/Database/RealmContextFactory.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 2bc77934a8..08d7580db7 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -52,6 +52,8 @@ namespace osu.Game.Database /// private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1); + private bool canCreateContexts; + private static readonly GlobalStatistic refreshes = GlobalStatistics.Get(@"Realm", @"Dirty Refreshes"); private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get(@"Realm", @"Contexts (Created)"); @@ -153,7 +155,13 @@ namespace osu.Game.Database try { - contextCreationLock.Wait(); + if (!canCreateContexts) + contextCreationLock.Wait(); + + // the semaphore is used to stop all context creation. + // once the semaphore has been taken by this code section, it is safe to create further contexts. + // this can happen if a realm subscription is active and triggers a callback which has user code that calls `CreateContext`. + canCreateContexts = true; contexts_created.Value++; @@ -162,6 +170,7 @@ namespace osu.Game.Database finally { contextCreationLock.Release(); + canCreateContexts = false; } } From 673481ebc5eaece0e99f0bc61124655fd3944d4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 17:38:42 +0900 Subject: [PATCH 029/419] Use `ThreadLocal` to avoid potential threading issues --- osu.Game/Database/RealmContextFactory.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 08d7580db7..9e66248d78 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -52,7 +52,7 @@ namespace osu.Game.Database /// private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1); - private bool canCreateContexts; + private ThreadLocal currentThreadCanCreateContexts = new ThreadLocal(); private static readonly GlobalStatistic refreshes = GlobalStatistics.Get(@"Realm", @"Dirty Refreshes"); private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get(@"Realm", @"Contexts (Created)"); @@ -155,13 +155,13 @@ namespace osu.Game.Database try { - if (!canCreateContexts) + if (!currentThreadCanCreateContexts.Value) contextCreationLock.Wait(); // the semaphore is used to stop all context creation. // once the semaphore has been taken by this code section, it is safe to create further contexts. // this can happen if a realm subscription is active and triggers a callback which has user code that calls `CreateContext`. - canCreateContexts = true; + currentThreadCanCreateContexts.Value = true; contexts_created.Value++; @@ -170,7 +170,7 @@ namespace osu.Game.Database finally { contextCreationLock.Release(); - canCreateContexts = false; + currentThreadCanCreateContexts.Value = false; } } From f19cfcc82e8decff5d63c1d4e098cfcb746e7b03 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 17:38:54 +0900 Subject: [PATCH 030/419] Make `readonly` --- osu.Game/Database/RealmContextFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 9e66248d78..b1e97a45d0 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -52,7 +52,7 @@ namespace osu.Game.Database /// private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1); - private ThreadLocal currentThreadCanCreateContexts = new ThreadLocal(); + private readonly ThreadLocal currentThreadCanCreateContexts = new ThreadLocal(); private static readonly GlobalStatistic refreshes = GlobalStatistics.Get(@"Realm", @"Dirty Refreshes"); private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get(@"Realm", @"Contexts (Created)"); From 23fded4a3ac22be9eb0f92a38dc173402feed386 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 18:26:37 +0900 Subject: [PATCH 031/419] Fix potential oversight in semaphore release logic --- osu.Game/Database/RealmContextFactory.cs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index b1e97a45d0..6948918fe7 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -153,15 +153,22 @@ namespace osu.Game.Database if (isDisposed) throw new ObjectDisposedException(nameof(RealmContextFactory)); + bool tookSemaphoreLock = false; + try { if (!currentThreadCanCreateContexts.Value) + { contextCreationLock.Wait(); - - // the semaphore is used to stop all context creation. - // once the semaphore has been taken by this code section, it is safe to create further contexts. - // this can happen if a realm subscription is active and triggers a callback which has user code that calls `CreateContext`. - currentThreadCanCreateContexts.Value = true; + tookSemaphoreLock = true; + } + else + { + // the semaphore is used to handle blocking of all context creation during certain periods. + // once the semaphore has been taken by this code section, it is safe to create further contexts on the same thread. + // this can happen if a realm subscription is active and triggers a callback which has user code that calls `CreateContext`. + currentThreadCanCreateContexts.Value = true; + } contexts_created.Value++; @@ -169,8 +176,11 @@ namespace osu.Game.Database } finally { - contextCreationLock.Release(); - currentThreadCanCreateContexts.Value = false; + if (tookSemaphoreLock) + { + contextCreationLock.Release(); + currentThreadCanCreateContexts.Value = false; + } } } From 1dd5e1ca89d78190cea96f56d0d05ee691ac7908 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Nov 2021 14:43:35 +0900 Subject: [PATCH 032/419] Remove `RealmLive` context re-fetch optimisation for now --- osu.Game/Database/RealmLive.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 5ee40f5b4d..45b598ed92 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -112,7 +112,10 @@ namespace osu.Game.Database } } - private bool originalDataValid => !IsManaged || (isCorrectThread && data.IsValid); + // TODO: Revisit adding these conditionals back as an optimisation: || (isCorrectThread && data.IsValid); + // They have temporarily been removed due to an oversight involving .AsQueryable, see https://github.com/realm/realm-dotnet/discussions/2734. + // This means we are fetching a new context every `PerformRead` or `PerformWrite`, even when on the correct thread. + private bool originalDataValid => !IsManaged; // this matches realm's internal thread validation (see https://github.com/realm/realm-dotnet/blob/903b4d0b304f887e37e2d905384fb572a6496e70/Realm/Realm/Native/SynchronizationContextScheduler.cs#L72) private bool isCorrectThread From 348d1d0be910ccacf482360150c6b77634af560b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Nov 2021 21:33:11 +0900 Subject: [PATCH 033/419] Don't dispose fetched realm instance when using `RealmLive.Value` See https://github.com/realm/realm-dotnet/discussions/2734#discussioncomment-1705038 for reasoning. --- osu.Game/Database/RealmLive.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 45b598ed92..6d5410d1cd 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -100,10 +100,11 @@ namespace osu.Game.Database if (originalDataValid) return data; - T retrieved; + if (!isCorrectThread) + throw new InvalidOperationException($"Can't use {nameof(Value)} unless on the same thread the original data was fetched from."); - using (var realm = Realm.GetInstance(data.Realm.Config)) - retrieved = realm.Find(ID); + var realm = Realm.GetInstance(data.Realm.Config); + var retrieved = realm.Find(ID); if (!retrieved.IsValid) throw new InvalidOperationException("Attempted to access value without an open context"); From 4c2b0dd2d1168df00120016d02af29861eb9052a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 15:14:27 +0900 Subject: [PATCH 034/419] Update `RealmLive` tests in line with modified behaviour --- osu.Game.Tests/Database/RealmLiveTests.cs | 68 ++++++++++++++--------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 16e2c0fc6a..baedf44c1a 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -82,19 +82,29 @@ namespace osu.Game.Tests.Database Task.Factory.StartNew(() => { - Assert.DoesNotThrow(() => + // TODO: The commented code is the behaviour we hope to obtain, but is temporarily disabled. + // See https://github.com/ppy/osu/pull/15851 + using (realmFactory.CreateContext()) { - using (realmFactory.CreateContext()) + Assert.Throws(() => { - var resolved = liveBeatmap.Value; + var __ = liveBeatmap.Value; + }); + } - Assert.IsTrue(resolved.Realm.IsClosed); - Assert.IsTrue(resolved.IsValid); - - // can access properties without a crash. - Assert.IsFalse(resolved.Hidden); - } - }); + // Assert.DoesNotThrow(() => + // { + // using (realmFactory.CreateContext()) + // { + // var resolved = liveBeatmap.Value; + // + // Assert.IsTrue(resolved.Realm.IsClosed); + // Assert.IsTrue(resolved.IsValid); + // + // // can access properties without a crash. + // Assert.IsFalse(resolved.Hidden); + // } + // }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); }); } @@ -215,23 +225,29 @@ namespace osu.Game.Tests.Database Assert.AreEqual(0, updateThreadContext.All().Count()); Assert.AreEqual(0, changesTriggered); - var resolved = liveBeatmap.Value; - - // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point. - Assert.AreEqual(2, updateThreadContext.All().Count()); - Assert.AreEqual(1, changesTriggered); - - // even though the realm that this instance was resolved for was closed, it's still valid. - Assert.IsTrue(resolved.Realm.IsClosed); - Assert.IsTrue(resolved.IsValid); - - // can access properties without a crash. - Assert.IsFalse(resolved.Hidden); - - updateThreadContext.Write(r => + // TODO: Originally the following was using `liveBeatmap.Value`. This has been temporarily disabled. + // See https://github.com/ppy/osu/pull/15851 + liveBeatmap.PerformRead(resolved => { - // can use with the main context. - r.Remove(resolved); + // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point. + // ReSharper disable once AccessToDisposedClosure + Assert.AreEqual(2, updateThreadContext.All().Count()); + Assert.AreEqual(1, changesTriggered); + + // TODO: as above, temporarily disabled as it doesn't make sense inside a `PerformRead`. + // // even though the realm that this instance was resolved for was closed, it's still valid. + // Assert.IsTrue(resolved.Realm.IsClosed); + // Assert.IsTrue(resolved.IsValid); + + // can access properties without a crash. + Assert.IsFalse(resolved.Hidden); + + // ReSharper disable once AccessToDisposedClosure + updateThreadContext.Write(r => + { + // can use with the main context. + r.Remove(resolved); + }); }); } From 0a961fd9d8d2da027f88904e575ec952d5535b7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Nov 2021 13:24:43 +0900 Subject: [PATCH 035/419] Replace usages of `IHasFiles` with `IHasRealmFiles` --- osu.Game/Skinning/DefaultLegacySkin.cs | 2 +- osu.Game/Skinning/LegacyBeatmapSkin.cs | 2 +- osu.Game/Skinning/LegacySkin.cs | 2 +- osu.Game/Skinning/LegacySkinResourceStore.cs | 9 ++++----- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index cd6dbd9ddd..95e9a19c99 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -25,7 +25,7 @@ namespace osu.Game.Skinning resources, // A default legacy skin may still have a skin.ini if it is modified by the user. // We must specify the stream directly as we are redirecting storage to the osu-resources location for other files. - new LegacySkinResourceStore(skin, resources.Files).GetStream("skin.ini") + new LegacySkinResourceStore(skin, resources.Files).GetStream("skin.ini") ) { Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 8abef6800d..0599334b46 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -21,7 +21,7 @@ namespace osu.Game.Skinning protected override bool UseCustomSampleBanks => true; public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, IResourceStore storage, IStorageResourceProvider resources) - : base(createSkinInfo(beatmapInfo), new LegacySkinResourceStore(beatmapInfo.BeatmapSet, storage), resources, beatmapInfo.Path) + : base(createSkinInfo(beatmapInfo), new LegacySkinResourceStore(beatmapInfo.BeatmapSet, storage), resources, beatmapInfo.Path) { // Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer) Configuration.AllowDefaultComboColoursFallback = false; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 0e7ae95169..64f920de85 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -51,7 +51,7 @@ namespace osu.Game.Skinning [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] public LegacySkin(SkinInfo skin, IStorageResourceProvider resources) - : this(skin, new LegacySkinResourceStore(skin, resources.Files), resources, "skin.ini") + : this(skin, new LegacySkinResourceStore(skin, resources.Files), resources, "skin.ini") { } diff --git a/osu.Game/Skinning/LegacySkinResourceStore.cs b/osu.Game/Skinning/LegacySkinResourceStore.cs index c4418baeff..71efc949b8 100644 --- a/osu.Game/Skinning/LegacySkinResourceStore.cs +++ b/osu.Game/Skinning/LegacySkinResourceStore.cs @@ -11,12 +11,11 @@ using osu.Game.Extensions; namespace osu.Game.Skinning { - public class LegacySkinResourceStore : ResourceStore - where T : INamedFileInfo + public class LegacySkinResourceStore : ResourceStore { - private readonly IHasFiles source; + private readonly IHasRealmFiles source; - public LegacySkinResourceStore(IHasFiles source, IResourceStore underlyingStore) + public LegacySkinResourceStore(IHasRealmFiles source, IResourceStore underlyingStore) : base(underlyingStore) { this.source = source; @@ -33,7 +32,7 @@ namespace osu.Game.Skinning } private string getPathForFile(string filename) => - source.Files.Find(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.FileInfo.GetStoragePath(); + source.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.FileInfo.GetStoragePath(); public override IEnumerable GetAvailableResources() => source.Files.Select(f => f.Filename); } From 5f067172b41e243aa79d8d25bac024b551212ce6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Nov 2021 14:54:57 +0900 Subject: [PATCH 036/419] Add model class for realm skin --- osu.Game/Models/RealmSkin.cs | 67 ++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 osu.Game/Models/RealmSkin.cs diff --git a/osu.Game/Models/RealmSkin.cs b/osu.Game/Models/RealmSkin.cs new file mode 100644 index 0000000000..2ae1328974 --- /dev/null +++ b/osu.Game/Models/RealmSkin.cs @@ -0,0 +1,67 @@ +// 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.Extensions.ObjectExtensions; +using osu.Framework.Testing; +using osu.Game.Database; +using osu.Game.Extensions; +using osu.Game.IO; +using osu.Game.Skinning; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [ExcludeFromDynamicCompile] + [MapTo("Skin")] + public class RealmSkin : RealmObject, IHasRealmFiles, IEquatable, IHasGuidPrimaryKey, ISoftDelete + { + public Guid ID { get; set; } + + public string Name { get; set; } = string.Empty; + + public string Creator { get; set; } = string.Empty; + + public string Hash { get; set; } = string.Empty; + + public string InstantiationInfo { get; set; } = string.Empty; + + public virtual Skin CreateInstance(IStorageResourceProvider resources) + { + var type = string.IsNullOrEmpty(InstantiationInfo) + // handle the case of skins imported before InstantiationInfo was added. + ? typeof(LegacySkin) + : Type.GetType(InstantiationInfo).AsNonNull(); + + return (Skin)Activator.CreateInstance(type, this, resources); + } + + public IList Files { get; } = null!; + + public bool DeletePending { get; set; } + + public static RealmSkin Default { get; } = new RealmSkin + { + Name = "osu! (triangles)", + Creator = "team osu!", + InstantiationInfo = typeof(DefaultSkin).GetInvariantInstantiationInfo() + }; + + public bool Equals(RealmSkin? other) + { + if (ReferenceEquals(this, other)) return true; + if (other == null) return false; + + return ID == other.ID; + } + + public override string ToString() + { + string author = string.IsNullOrEmpty(Creator) ? string.Empty : $"({Creator})"; + return $"{Name} {author}".Trim(); + } + } +} From e283379f0ebaa703bf613a9108604a98c162bd91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Nov 2021 15:31:07 +0900 Subject: [PATCH 037/419] Replace EF `SkinInfo` with realm implementation --- osu.Game/Database/OsuDbContext.cs | 6 +-- .../RealmSkin.cs => Skinning/EFSkinInfo.cs} | 40 ++++++++----------- osu.Game/Skinning/SkinInfo.cs | 37 +++++++++++------ osu.Game/Skinning/SkinStore.cs | 2 +- 4 files changed, 46 insertions(+), 39 deletions(-) rename osu.Game/{Models/RealmSkin.cs => Skinning/EFSkinInfo.cs} (54%) diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index d8d2cb8981..26f287da26 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -25,7 +25,7 @@ namespace osu.Game.Database public DbSet BeatmapSetInfo { get; set; } public DbSet FileInfo { get; set; } public DbSet RulesetInfo { get; set; } - public DbSet SkinInfo { get; set; } + public DbSet SkinInfo { get; set; } public DbSet ScoreInfo { get; set; } // migrated to realm @@ -133,8 +133,8 @@ namespace osu.Game.Database modelBuilder.Entity().HasIndex(b => b.DeletePending); modelBuilder.Entity().HasIndex(b => b.Hash).IsUnique(); - modelBuilder.Entity().HasIndex(b => b.Hash).IsUnique(); - modelBuilder.Entity().HasIndex(b => b.DeletePending); + modelBuilder.Entity().HasIndex(b => b.Hash).IsUnique(); + modelBuilder.Entity().HasIndex(b => b.DeletePending); modelBuilder.Entity().HasIndex(b => new { b.RulesetID, b.Variant }); diff --git a/osu.Game/Models/RealmSkin.cs b/osu.Game/Skinning/EFSkinInfo.cs similarity index 54% rename from osu.Game/Models/RealmSkin.cs rename to osu.Game/Skinning/EFSkinInfo.cs index 2ae1328974..8bd734d4ff 100644 --- a/osu.Game/Models/RealmSkin.cs +++ b/osu.Game/Skinning/EFSkinInfo.cs @@ -1,33 +1,32 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.ComponentModel.DataAnnotations.Schema; using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Extensions; using osu.Game.IO; -using osu.Game.Skinning; -using Realms; -#nullable enable - -namespace osu.Game.Models +namespace osu.Game.Skinning { - [ExcludeFromDynamicCompile] - [MapTo("Skin")] - public class RealmSkin : RealmObject, IHasRealmFiles, IEquatable, IHasGuidPrimaryKey, ISoftDelete + [Table(@"SkinInfo")] + public class EFSkinInfo : IHasFiles, IEquatable, IHasPrimaryKey, ISoftDelete { - public Guid ID { get; set; } + internal const int DEFAULT_SKIN = 0; + internal const int CLASSIC_SKIN = -1; + internal const int RANDOM_SKIN = -2; + + public int ID { get; set; } public string Name { get; set; } = string.Empty; public string Creator { get; set; } = string.Empty; - public string Hash { get; set; } = string.Empty; + public string Hash { get; set; } - public string InstantiationInfo { get; set; } = string.Empty; + public string InstantiationInfo { get; set; } public virtual Skin CreateInstance(IStorageResourceProvider resources) { @@ -39,28 +38,23 @@ namespace osu.Game.Models return (Skin)Activator.CreateInstance(type, this, resources); } - public IList Files { get; } = null!; + public List Files { get; set; } = new List(); public bool DeletePending { get; set; } - public static RealmSkin Default { get; } = new RealmSkin + public static EFSkinInfo Default { get; } = new EFSkinInfo { + ID = DEFAULT_SKIN, Name = "osu! (triangles)", Creator = "team osu!", InstantiationInfo = typeof(DefaultSkin).GetInvariantInstantiationInfo() }; - public bool Equals(RealmSkin? other) - { - if (ReferenceEquals(this, other)) return true; - if (other == null) return false; - - return ID == other.ID; - } + public bool Equals(EFSkinInfo other) => other != null && ID == other.ID; public override string ToString() { - string author = string.IsNullOrEmpty(Creator) ? string.Empty : $"({Creator})"; + string author = Creator == null ? string.Empty : $"({Creator})"; return $"{Name} {author}".Trim(); } } diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index 5d2d51a9b0..a83e7f5b1e 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -1,30 +1,38 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Extensions.ObjectExtensions; +using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Extensions; using osu.Game.IO; +using osu.Game.Models; +using Realms; + +#nullable enable namespace osu.Game.Skinning { - public class SkinInfo : IHasFiles, IEquatable, IHasPrimaryKey, ISoftDelete, IHasNamedFiles + [ExcludeFromDynamicCompile] + [MapTo("Skin")] + public class SkinInfo : RealmObject, IHasRealmFiles, IEquatable, IHasGuidPrimaryKey, ISoftDelete, IHasNamedFiles { - internal const int DEFAULT_SKIN = 0; - internal const int CLASSIC_SKIN = -1; - internal const int RANDOM_SKIN = -2; + internal static readonly Guid DEFAULT_SKIN = new Guid("2991CFD8-2140-469A-BCB9-2EC23FBCE4AD"); + internal static readonly Guid CLASSIC_SKIN = new Guid("81F02CD3-EEC6-4865-AC23-FAE26A386187"); + internal static readonly Guid RANDOM_SKIN = new Guid("D39DFEFB-477C-4372-B1EA-2BCEA5FB8908"); - public int ID { get; set; } + [PrimaryKey] + public Guid ID { get; set; } = Guid.NewGuid(); public string Name { get; set; } = string.Empty; public string Creator { get; set; } = string.Empty; - public string Hash { get; set; } + public string Hash { get; set; } = string.Empty; - public string InstantiationInfo { get; set; } + public string InstantiationInfo { get; set; } = string.Empty; public virtual Skin CreateInstance(IStorageResourceProvider resources) { @@ -36,23 +44,28 @@ namespace osu.Game.Skinning return (Skin)Activator.CreateInstance(type, this, resources); } - public List Files { get; } = new List(); + public IList Files { get; } = null!; public bool DeletePending { get; set; } public static SkinInfo Default { get; } = new SkinInfo { - ID = DEFAULT_SKIN, Name = "osu! (triangles)", Creator = "team osu!", InstantiationInfo = typeof(DefaultSkin).GetInvariantInstantiationInfo() }; - public bool Equals(SkinInfo other) => other != null && ID == other.ID; + public bool Equals(SkinInfo? other) + { + if (ReferenceEquals(this, other)) return true; + if (other == null) return false; + + return ID == other.ID; + } public override string ToString() { - string author = Creator == null ? string.Empty : $"({Creator})"; + string author = string.IsNullOrEmpty(Creator) ? string.Empty : $"({Creator})"; return $"{Name} {author}".Trim(); } diff --git a/osu.Game/Skinning/SkinStore.cs b/osu.Game/Skinning/SkinStore.cs index 31cadb0a24..922d146259 100644 --- a/osu.Game/Skinning/SkinStore.cs +++ b/osu.Game/Skinning/SkinStore.cs @@ -6,7 +6,7 @@ using osu.Game.Database; namespace osu.Game.Skinning { - public class SkinStore : MutableDatabaseBackedStoreWithFileIncludes + public class SkinStore : MutableDatabaseBackedStoreWithFileIncludes { public SkinStore(DatabaseContextFactory contextFactory, Storage storage = null) : base(contextFactory, storage) From 3db5646fa846e5c6d2550915e626d1fda5c0ed66 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Nov 2021 16:04:55 +0900 Subject: [PATCH 038/419] Create Guid constants for system skins (and store skin choice to configuration as guid) --- osu.Game/Configuration/OsuConfigManager.cs | 11 +++++--- osu.Game/OsuGame.cs | 26 ++++++++----------- .../Overlays/Settings/Sections/SkinSection.cs | 20 +++++++------- osu.Game/Skinning/SkinInfo.cs | 1 + 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 84da3f666d..ade817e652 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -27,7 +27,7 @@ namespace osu.Game.Configuration { // UI/selection defaults SetDefault(OsuSetting.Ruleset, string.Empty); - SetDefault(OsuSetting.Skin, 0, -1, int.MaxValue); + SetDefault(OsuSetting.Skin, string.Empty); SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details); SetDefault(OsuSetting.BeatmapDetailModsFilter, false); @@ -210,9 +210,12 @@ namespace osu.Game.Configuration value: scalingMode.GetLocalisableDescription() ) ), - new TrackedSetting(OsuSetting.Skin, skin => + new TrackedSetting(OsuSetting.Skin, skin => { - string skinName = LookupSkinName(skin) ?? string.Empty; + string skinName = string.Empty; + + if (Guid.TryParse(skin, out var id)) + skinName = LookupSkinName(id) ?? string.Empty; return new SettingDescription( rawValue: skinName, @@ -233,7 +236,7 @@ namespace osu.Game.Configuration }; } - public Func LookupSkinName { private get; set; } + public Func LookupSkinName { private get; set; } public Func LookupKeyBindings { get; set; } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 99b67976e3..c294efa647 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -161,7 +161,7 @@ namespace osu.Game private Bindable uiScale; - private Bindable configSkin; + private Bindable configSkin; private readonly string[] args; @@ -243,27 +243,23 @@ namespace osu.Game Ruleset.ValueChanged += r => configRuleset.Value = r.NewValue.ShortName; // bind config int to database SkinInfo - configSkin = LocalConfig.GetBindable(OsuSetting.Skin); - SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID; + configSkin = LocalConfig.GetBindable(OsuSetting.Skin); + SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString(); configSkin.ValueChanged += skinId => { - var skinInfo = SkinManager.Query(s => s.ID == skinId.NewValue); + // TODO: migrate the user skin selection to the new ID format. + SkinInfo skinInfo = null; + + if (Guid.TryParse(skinId.NewValue, out var guid)) + skinInfo = SkinManager.Query(s => s.ID == guid); if (skinInfo == null) { - switch (skinId.NewValue) - { - case -1: - skinInfo = DefaultLegacySkin.Info; - break; - - default: - skinInfo = SkinInfo.Default; - break; - } + if (guid == SkinInfo.CLASSIC_SKIN) + skinInfo = DefaultLegacySkin.Info; } - SkinManager.CurrentSkinInfo.Value = skinInfo; + SkinManager.CurrentSkinInfo.Value = skinInfo ?? SkinInfo.Default; }; configSkin.TriggerChange(); diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 0eb65b4b0f..168214bbde 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Settings.Sections }; private readonly Bindable dropdownBindable = new Bindable { Default = SkinInfo.Default }; - private readonly Bindable configBindable = new Bindable(); + private readonly Bindable configBindable = new Bindable(); private static readonly SkinInfo random_skin_info = new SkinInfo { @@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Settings.Sections { get { - int index = skinItems.FindIndex(s => s.ID > 0); + int index = skinItems.FindIndex(s => s.ID == SkinInfo.CLASSIC_SKIN); if (index < 0) index = skinItems.Count; @@ -84,33 +84,33 @@ namespace osu.Game.Overlays.Settings.Sections updateItems(); // Todo: This should not be necessary when OsuConfigManager is databased - if (skinDropdown.Items.All(s => s.ID != configBindable.Value)) - configBindable.Value = 0; + if (!Guid.TryParse(configBindable.Value, out var configId) || skinDropdown.Items.All(s => s.ID != configId)) + configBindable.Value = string.Empty; configBindable.BindValueChanged(id => Scheduler.AddOnce(updateSelectedSkinFromConfig), true); dropdownBindable.BindValueChanged(skin => { - if (skin.NewValue == random_skin_info) + if (skin.NewValue.Equals(random_skin_info)) { skins.SelectRandomSkin(); return; } - configBindable.Value = skin.NewValue.ID; + configBindable.Value = skin.NewValue.ID.ToString(); }); } private void updateSelectedSkinFromConfig() { - int id = configBindable.Value; + if (!Guid.TryParse(configBindable.Value, out var configId)) return; - var skin = skinDropdown.Items.FirstOrDefault(s => s.ID == id); + var skin = skinDropdown.Items.FirstOrDefault(s => s.ID == configId); if (skin == null) { // there may be a thread race condition where an item is selected that hasn't yet been added to the dropdown. // to avoid adding complexity, let's just ensure the item is added so we can perform the selection. - skin = skins.Query(s => s.ID == id); + skin = skins.Query(s => s.ID == configId); addItem(skin); } @@ -181,7 +181,7 @@ namespace osu.Game.Overlays.Settings.Sections Action = export; currentSkin = skins.CurrentSkin.GetBoundCopy(); - currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.ID > 0, true); + currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.IsManaged, true); } private void export() diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index a83e7f5b1e..c09d45c227 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -50,6 +50,7 @@ namespace osu.Game.Skinning public static SkinInfo Default { get; } = new SkinInfo { + ID = DEFAULT_SKIN, Name = "osu! (triangles)", Creator = "team osu!", InstantiationInfo = typeof(DefaultSkin).GetInvariantInstantiationInfo() From 2a9c7c00c8a261c63bb1e8860066940fbad7a2a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Nov 2021 16:05:28 +0900 Subject: [PATCH 039/419] Update tests and file access code --- osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 4 ++-- .../Visual/Background/TestSceneBackgroundScreenDefault.cs | 2 +- osu.Game/Skinning/Skin.cs | 4 ++-- osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs | 7 ++----- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index ecc9c92025..64d7359480 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -181,14 +181,14 @@ namespace osu.Game.Tests.Skins.IO { Assert.That(import2.ID, Is.Not.EqualTo(import1.ID)); Assert.That(import2.Hash, Is.Not.EqualTo(import1.Hash)); - Assert.That(import2.Files.Select(f => f.FileInfoID), Is.Not.EquivalentTo(import1.Files.Select(f => f.FileInfoID))); + Assert.That(import2.Files.First(), Is.Not.EqualTo(import1.Files.First())); } private void assertImportedOnce(SkinInfo import1, SkinInfo import2) { Assert.That(import2.ID, Is.EqualTo(import1.ID)); Assert.That(import2.Hash, Is.EqualTo(import1.Hash)); - Assert.That(import2.Files.Select(f => f.FileInfoID), Is.EquivalentTo(import1.Files.Select(f => f.FileInfoID))); + Assert.That(import2.Files.First(), Is.EqualTo(import1.Files.First())); } private MemoryStream createEmptyOsk() diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs index ec16578b71..a56b69a024 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Background private void setCustomSkin() { // feign a skin switch. this doesn't do anything except force CurrentSkin to become a LegacySkin. - AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo { ID = 5 }); + AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo()); } private void setDefaultSkin() => AddStep("set default skin", () => skins.CurrentSkinInfo.SetDefault()); diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 10526b69af..2f16270fb7 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -64,7 +64,7 @@ namespace osu.Game.Skinning if (fileInfo == null) continue; - byte[] bytes = resources?.Files.Get(fileInfo.FileInfo.GetStoragePath()); + byte[] bytes = resources?.Files.Get(fileInfo.File.GetStoragePath()); if (bytes == null) continue; @@ -94,7 +94,7 @@ namespace osu.Game.Skinning private Stream getConfigurationStream() { - string path = SkinInfo.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase))?.FileInfo.GetStoragePath(); + string path = SkinInfo.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath(); if (string.IsNullOrEmpty(path)) return null; diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index 9d92f5c5fc..31a2071249 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -15,6 +15,7 @@ using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO; +using osu.Game.Models; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Ranking; @@ -88,11 +89,7 @@ namespace osu.Game.Tests.Beatmaps AddStep("setup skins", () => { userSkinInfo.Files.Clear(); - userSkinInfo.Files.Add(new SkinFileInfo - { - Filename = userFile, - FileInfo = new IO.FileInfo { Hash = userFile } - }); + userSkinInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = userFile }, userFile)); beatmapInfo.BeatmapSet.Files.Clear(); beatmapInfo.BeatmapSet.Files.Add(new BeatmapSetFileInfo From 23146d59d1b1ee9464e52723113ec86d798130f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 17:57:17 +0900 Subject: [PATCH 040/419] Use `ILive` for current skin --- .../Editor/TestSceneManiaComposeScreen.cs | 5 ++- osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 44 ++++++++++++------- .../Skins/TestSceneSkinResources.cs | 2 +- .../TestSceneBackgroundScreenDefault.cs | 3 +- .../Gameplay/TestSceneBeatmapSkinFallbacks.cs | 3 +- osu.Game/OsuGame.cs | 6 +-- osu.Game/Skinning/SkinManager.cs | 2 +- osu.Game/Tests/Visual/SkinnableTestScene.cs | 2 +- 8 files changed, 40 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs index 24d2a786a0..a7e76e44b5 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Game.Database; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Screens.Edit; @@ -55,13 +56,13 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor [Test] public void TestDefaultSkin() { - AddStep("set default skin", () => skins.CurrentSkinInfo.Value = SkinInfo.Default); + AddStep("set default skin", () => skins.CurrentSkinInfo.Value = SkinInfo.Default.ToLive()); } [Test] public void TestLegacySkin() { - AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info); + AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info.ToLive()); } } } diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index 64d7359480..9b7e169745 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Platform; +using osu.Game.Database; using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Skinning; @@ -165,30 +166,39 @@ namespace osu.Game.Tests.Skins.IO #endregion - private void assertCorrectMetadata(SkinInfo import1, string name, string creator, OsuGameBase osu) + private void assertCorrectMetadata(ILive import1, string name, string creator, OsuGameBase osu) { - Assert.That(import1.Name, Is.EqualTo(name)); - Assert.That(import1.Creator, Is.EqualTo(creator)); + import1.PerformRead(i => + { + Assert.That(i.Name, Is.EqualTo(name)); + Assert.That(i.Creator, Is.EqualTo(creator)); - // for extra safety let's reconstruct the skin, reading from the skin.ini. - var instance = import1.CreateInstance((IStorageResourceProvider)osu.Dependencies.Get(typeof(SkinManager))); + // for extra safety let's reconstruct the skin, reading from the skin.ini. + var instance = i.CreateInstance((IStorageResourceProvider)osu.Dependencies.Get(typeof(SkinManager))); - Assert.That(instance.Configuration.SkinInfo.Name, Is.EqualTo(name)); - Assert.That(instance.Configuration.SkinInfo.Creator, Is.EqualTo(creator)); + Assert.That(instance.Configuration.SkinInfo.Name, Is.EqualTo(name)); + Assert.That(instance.Configuration.SkinInfo.Creator, Is.EqualTo(creator)); + }); } - private void assertImportedBoth(SkinInfo import1, SkinInfo import2) + private void assertImportedBoth(ILive import1, ILive import2) { - Assert.That(import2.ID, Is.Not.EqualTo(import1.ID)); - Assert.That(import2.Hash, Is.Not.EqualTo(import1.Hash)); - Assert.That(import2.Files.First(), Is.Not.EqualTo(import1.Files.First())); + import1.PerformRead(i1 => import2.PerformRead(i2 => + { + Assert.That(i2.ID, Is.Not.EqualTo(i1.ID)); + Assert.That(i2.Hash, Is.Not.EqualTo(i1.Hash)); + Assert.That(i2.Files.First(), Is.Not.EqualTo(i1.Files.First())); + })); } - private void assertImportedOnce(SkinInfo import1, SkinInfo import2) + private void assertImportedOnce(ILive import1, ILive import2) { - Assert.That(import2.ID, Is.EqualTo(import1.ID)); - Assert.That(import2.Hash, Is.EqualTo(import1.Hash)); - Assert.That(import2.Files.First(), Is.EqualTo(import1.Files.First())); + import1.PerformRead(i1 => import2.PerformRead(i2 => + { + Assert.That(i2.ID, Is.EqualTo(i1.ID)); + Assert.That(i2.Hash, Is.EqualTo(i1.Hash)); + Assert.That(i2.Files.First(), Is.EqualTo(i1.Files.First())); + })); } private MemoryStream createEmptyOsk() @@ -255,10 +265,10 @@ namespace osu.Game.Tests.Skins.IO } } - private async Task loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null) + private async Task> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null) { var skinManager = osu.Dependencies.Get(); - return (await skinManager.Import(archive)).Value; + return (await skinManager.Import(archive)); } } } diff --git a/osu.Game.Tests/Skins/TestSceneSkinResources.cs b/osu.Game.Tests/Skins/TestSceneSkinResources.cs index 10f1ab31df..09535b76e3 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinResources.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Skins private void load() { var imported = skins.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-skin.osk"))).Result; - skin = skins.GetSkin(imported.Value); + skin = imported.PerformRead(skinInfo => skins.GetSkin(skinInfo)); } [Test] diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs index a56b69a024..bdd1b92c8d 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Graphics.Backgrounds; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -122,7 +123,7 @@ namespace osu.Game.Tests.Visual.Background private void setCustomSkin() { // feign a skin switch. this doesn't do anything except force CurrentSkin to become a LegacySkin. - AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo()); + AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo().ToLive()); } private void setDefaultSkin() => AddStep("set default skin", () => skins.CurrentSkinInfo.SetDefault()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index 7398527f57..c174a3edc2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -12,6 +12,7 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; @@ -52,7 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("setup skins", () => { - skinManager.CurrentSkinInfo.Value = gameCurrentSkin; + skinManager.CurrentSkinInfo.Value = gameCurrentSkin.ToLive(); currentBeatmapSkin = getBeatmapSkin(); }); }); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c294efa647..1ffbb4cb85 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -248,7 +248,7 @@ namespace osu.Game configSkin.ValueChanged += skinId => { // TODO: migrate the user skin selection to the new ID format. - SkinInfo skinInfo = null; + ILive skinInfo = null; if (Guid.TryParse(skinId.NewValue, out var guid)) skinInfo = SkinManager.Query(s => s.ID == guid); @@ -256,10 +256,10 @@ namespace osu.Game if (skinInfo == null) { if (guid == SkinInfo.CLASSIC_SKIN) - skinInfo = DefaultLegacySkin.Info; + skinInfo = DefaultLegacySkin.Info.ToLive(); } - SkinManager.CurrentSkinInfo.Value = skinInfo ?? SkinInfo.Default; + SkinManager.CurrentSkinInfo.Value = skinInfo ?? SkinInfo.Default.ToLive(); }; configSkin.TriggerChange(); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 26ff4457af..24ec454276 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -46,7 +46,7 @@ namespace osu.Game.Skinning private readonly IResourceStore resources; public readonly Bindable CurrentSkin = new Bindable(); - public readonly Bindable CurrentSkinInfo = new Bindable(SkinInfo.Default) { Default = SkinInfo.Default }; + public readonly Bindable> CurrentSkinInfo = new Bindable>(SkinInfo.Default.ToLive()) { Default = SkinInfo.Default.ToLive() }; private readonly SkinModelManager skinModelManager; diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 000e7194bc..cdd3e47930 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual }, new OsuSpriteText { - Text = skin?.SkinInfo?.Name ?? "none", + Text = skin?.SkinInfo?.Value.Name ?? "none", Scale = new Vector2(1.5f), Padding = new MarginPadding(5), }, From 5db7cf23d35a75f3af71cfd22303efffe352be3a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 17:59:41 +0900 Subject: [PATCH 041/419] Add pending deletion skin cleanup --- osu.Game/Database/RealmContextFactory.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 6948918fe7..911c88a61f 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -14,6 +14,7 @@ using osu.Framework.Statistics; using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Models; +using osu.Game.Skinning; using osu.Game.Stores; using Realms; @@ -122,6 +123,11 @@ namespace osu.Game.Database realm.Remove(s); } + var pendingDeleteSkins = realm.All().Where(s => s.DeletePending); + + foreach (var s in pendingDeleteSkins) + realm.Remove(s); + transaction.Commit(); } From cd0c811ab1fdb40cd745b1913f5e7e3f727b229f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 18:00:03 +0900 Subject: [PATCH 042/419] Add the ability to call `ToString` on a `RealmLive` to get the underlying object's implementation --- osu.Game/Database/RealmLive.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 6d5410d1cd..6f17493097 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -123,5 +123,7 @@ namespace osu.Game.Database => (fetchedContext != null && SynchronizationContext.Current == fetchedContext) || fetchedThreadId == Thread.CurrentThread.ManagedThreadId; public bool Equals(ILive? other) => ID == other?.ID; + + public override string ToString() => PerformRead(i => i.ToString()); } } From 6b55de2845d22e8f14778bf764ff1208292682e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 18:02:09 +0900 Subject: [PATCH 043/419] Use `ILive` for `Skin.SkinInfo` --- osu.Game/Skinning/Skin.cs | 61 +++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 2f16270fb7..54fc2340f1 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.Logging; using osu.Game.Audio; +using osu.Game.Database; using osu.Game.Extensions; using osu.Game.IO; using osu.Game.Screens.Play.HUD; @@ -23,7 +24,7 @@ namespace osu.Game.Skinning { public abstract class Skin : IDisposable, ISkin { - public readonly SkinInfo SkinInfo; + public readonly ILive SkinInfo; private readonly IStorageResourceProvider resources; public SkinConfiguration Configuration { get; set; } @@ -42,7 +43,7 @@ namespace osu.Game.Skinning protected Skin(SkinInfo skin, IStorageResourceProvider resources, [CanBeNull] Stream configurationStream = null) { - SkinInfo = skin; + SkinInfo = skin.ToLive(); this.resources = resources; configurationStream ??= getConfigurationStream(); @@ -53,37 +54,41 @@ namespace osu.Game.Skinning else Configuration = new SkinConfiguration(); - // we may want to move this to some kind of async operation in the future. - foreach (SkinnableTarget skinnableTarget in Enum.GetValues(typeof(SkinnableTarget))) + // skininfo files may be null for default skin. + SkinInfo.PerformRead(s => { - string filename = $"{skinnableTarget}.json"; - - // skininfo files may be null for default skin. - var fileInfo = SkinInfo.Files.FirstOrDefault(f => f.Filename == filename); - - if (fileInfo == null) - continue; - - byte[] bytes = resources?.Files.Get(fileInfo.File.GetStoragePath()); - - if (bytes == null) - continue; - - try + // we may want to move this to some kind of async operation in the future. + foreach (SkinnableTarget skinnableTarget in Enum.GetValues(typeof(SkinnableTarget))) { - string jsonContent = Encoding.UTF8.GetString(bytes); - var deserializedContent = JsonConvert.DeserializeObject>(jsonContent); + string filename = $"{skinnableTarget}.json"; - if (deserializedContent == null) + // skininfo files may be null for default skin. + var fileInfo = s.Files.FirstOrDefault(f => f.Filename == filename); + + if (fileInfo == null) continue; - DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray(); + byte[] bytes = resources?.Files.Get(fileInfo.File.GetStoragePath()); + + if (bytes == null) + continue; + + try + { + string jsonContent = Encoding.UTF8.GetString(bytes); + var deserializedContent = JsonConvert.DeserializeObject>(jsonContent); + + if (deserializedContent == null) + continue; + + DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray(); + } + catch (Exception ex) + { + Logger.Error(ex, "Failed to load skin configuration."); + } } - catch (Exception ex) - { - Logger.Error(ex, "Failed to load skin configuration."); - } - } + }); } protected virtual void ParseConfigurationStream(Stream stream) @@ -94,7 +99,7 @@ namespace osu.Game.Skinning private Stream getConfigurationStream() { - string path = SkinInfo.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath(); + string path = SkinInfo.PerformRead(s => s.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath()); if (string.IsNullOrEmpty(path)) return null; From 744a5b33f5c261e124de28759ead387a52f4093b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 18:04:07 +0900 Subject: [PATCH 044/419] Rewrite `SkinSection` to use realm subscriptions and databased defaults --- .../Sections/Maintenance/GeneralSettings.cs | 5 +- .../Overlays/Settings/Sections/SkinSection.cs | 47 ++++++++++++------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index acdf9cdea6..e7e196555f 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -106,7 +106,10 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance dialogOverlay?.Push(new MassDeleteConfirmationDialog(() => { deleteSkinsButton.Enabled.Value = false; - Task.Run(() => skins.Delete(skins.GetAllUserSkins())).ContinueWith(t => Schedule(() => deleteSkinsButton.Enabled.Value = true)); + Task.Run(() => + { + skins.Delete(s => !s.DeletePending); + }).ContinueWith(t => Schedule(() => deleteSkinsButton.Enabled.Value = true)); })); } }); diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 168214bbde..9c5a2dff47 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -32,16 +32,16 @@ namespace osu.Game.Overlays.Settings.Sections Icon = FontAwesome.Solid.PaintBrush }; - private readonly Bindable dropdownBindable = new Bindable { Default = SkinInfo.Default }; + private readonly Bindable> dropdownBindable = new Bindable> { Default = SkinInfo.Default.ToLive() }; private readonly Bindable configBindable = new Bindable(); - private static readonly SkinInfo random_skin_info = new SkinInfo + private static readonly ILive random_skin_info = new SkinInfo { ID = SkinInfo.RANDOM_SKIN, Name = "", - }; + }.ToLive(); - private List skinItems; + private List> skinItems; private int firstNonDefaultSkinIndex { @@ -79,6 +79,11 @@ namespace osu.Game.Overlays.Settings.Sections skins.ItemRemoved += itemRemoved; config.BindWith(OsuSetting.Skin, configBindable); + } + + protected override void LoadComplete() + { + base.LoadComplete(); skinDropdown.Current = dropdownBindable; updateItems(); @@ -125,22 +130,30 @@ namespace osu.Game.Overlays.Settings.Sections skinDropdown.Items = skinItems; } - private void itemUpdated(SkinInfo item) => Schedule(() => addItem(item)); + private void itemUpdated(SkinInfo item) => Schedule(() => addItem(item.ToLive())); - private void addItem(SkinInfo item) + private void addItem(ILive item) { - List newDropdownItems = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToList(); + List> newDropdownItems = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToList(); sortUserSkins(newDropdownItems); skinDropdown.Items = newDropdownItems; } - private void itemRemoved(SkinInfo item) => Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => !i.Equals(item)).ToArray()); + private void itemRemoved(SkinInfo item) => Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => !i.ID.Equals(item.ID)).ToArray()); - private void sortUserSkins(List skinsList) + private void sortUserSkins(List> skinsList) { - // Sort user skins separately from built-in skins - skinsList.Sort(firstNonDefaultSkinIndex, skinsList.Count - firstNonDefaultSkinIndex, - Comparer.Create((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase))); + try + { + // Sort user skins separately from built-in skins + skinsList.Sort(firstNonDefaultSkinIndex, skinsList.Count - firstNonDefaultSkinIndex, + Comparer>.Create((a, b) => + { + // o_________________________o + return a.PerformRead(ai => b.PerformRead(bi => string.Compare(ai.Name, bi.Name, StringComparison.OrdinalIgnoreCase))); + })); + } + catch { } } protected override void Dispose(bool isDisposing) @@ -154,13 +167,13 @@ namespace osu.Game.Overlays.Settings.Sections } } - private class SkinSettingsDropdown : SettingsDropdown + private class SkinSettingsDropdown : SettingsDropdown> { - protected override OsuDropdown CreateDropdown() => new SkinDropdownControl(); + protected override OsuDropdown> CreateDropdown() => new SkinDropdownControl(); private class SkinDropdownControl : DropdownControl { - protected override LocalisableString GenerateItemText(SkinInfo item) => item.ToString(); + protected override LocalisableString GenerateItemText(ILive item) => item.ToString(); } } @@ -181,14 +194,14 @@ namespace osu.Game.Overlays.Settings.Sections Action = export; currentSkin = skins.CurrentSkin.GetBoundCopy(); - currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.IsManaged, true); + currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.PerformRead(s => s.IsManaged), true); } private void export() { try { - new LegacySkinExporter(storage).Export(currentSkin.Value.SkinInfo); + currentSkin.Value.SkinInfo.PerformRead(s => new LegacySkinExporter(storage).Export(s)); } catch (Exception e) { From e2d9a685d7ed48525a9c73465d0fb326ef25c283 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 18:05:36 +0900 Subject: [PATCH 045/419] Update skin implementations to match new structures --- osu.Game/Skinning/DefaultLegacySkin.cs | 4 +- osu.Game/Skinning/DefaultSkin.cs | 2 +- osu.Game/Skinning/LegacyBeatmapSkin.cs | 2 +- .../LegacyDatabasedSkinResourceStore.cs | 49 +++++++++++++++++++ osu.Game/Skinning/LegacySkin.cs | 2 +- osu.Game/Skinning/LegacySkinResourceStore.cs | 6 +-- 6 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index 95e9a19c99..2332e6e160 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -25,7 +25,7 @@ namespace osu.Game.Skinning resources, // A default legacy skin may still have a skin.ini if it is modified by the user. // We must specify the stream directly as we are redirecting storage to the osu-resources location for other files. - new LegacySkinResourceStore(skin, resources.Files).GetStream("skin.ini") + new LegacyDatabasedSkinResourceStore(skin, resources.Files).GetStream("skin.ini") ) { Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); @@ -42,7 +42,7 @@ namespace osu.Game.Skinning public static SkinInfo Info { get; } = new SkinInfo { - ID = SkinInfo.CLASSIC_SKIN, // this is temporary until database storage is decided upon. + ID = osu.Game.Skinning.SkinInfo.CLASSIC_SKIN, // this is temporary until database storage is decided upon. Name = "osu!classic", Creator = "team osu!", InstantiationInfo = typeof(DefaultLegacySkin).GetInvariantInstantiationInfo() diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index c377f16f8b..8d3a83f589 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -26,7 +26,7 @@ namespace osu.Game.Skinning private readonly IStorageResourceProvider resources; public DefaultSkin(IStorageResourceProvider resources) - : this(SkinInfo.Default, resources) + : this(osu.Game.Skinning.SkinInfo.Default, resources) { } diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 0599334b46..d44d3dce49 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -77,6 +77,6 @@ namespace osu.Game.Skinning } private static SkinInfo createSkinInfo(BeatmapInfo beatmapInfo) => - new SkinInfo { Name = beatmapInfo.ToString(), Creator = beatmapInfo.Metadata?.Author.Username }; + new SkinInfo { Name = beatmapInfo.ToString(), Creator = beatmapInfo.Metadata?.Author.Username ?? string.Empty }; } } diff --git a/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs b/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs new file mode 100644 index 0000000000..ab820a13ab --- /dev/null +++ b/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions; +using osu.Framework.IO.Stores; +using osu.Game.Database; +using osu.Game.Extensions; +using Realms; + +namespace osu.Game.Skinning +{ + public class LegacyDatabasedSkinResourceStore : ResourceStore + { + private readonly ILive source; + + public LegacyDatabasedSkinResourceStore(SkinInfo source, IResourceStore underlyingStore) + : base(underlyingStore) + { + this.source = source.ToLive(); + } + + protected override IEnumerable GetFilenames(string name) + { + foreach (string filename in base.GetFilenames(name)) + { + string path = getPathForFile(filename.ToStandardisedPath()); + if (path != null) + yield return path; + } + } + + private string getPathForFile(string filename) => + source.PerformRead(s => + { + if (s.IsManaged) + { + // avoid enumerating all files if this is a managed realm instance. + return s.Files.Filter(@"Filename ==[c] $0", filename).FirstOrDefault()?.File.GetStoragePath(); + } + + return s.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath(); + }); + + public override IEnumerable GetAvailableResources() => source.PerformRead(s => s.Files.Select(f => f.Filename)); + } +} diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 64f920de85..e677e2c01b 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -51,7 +51,7 @@ namespace osu.Game.Skinning [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] public LegacySkin(SkinInfo skin, IStorageResourceProvider resources) - : this(skin, new LegacySkinResourceStore(skin, resources.Files), resources, "skin.ini") + : this(skin, new LegacyDatabasedSkinResourceStore(skin, resources.Files), resources, "skin.ini") { } diff --git a/osu.Game/Skinning/LegacySkinResourceStore.cs b/osu.Game/Skinning/LegacySkinResourceStore.cs index 71efc949b8..2487a469c8 100644 --- a/osu.Game/Skinning/LegacySkinResourceStore.cs +++ b/osu.Game/Skinning/LegacySkinResourceStore.cs @@ -13,9 +13,9 @@ namespace osu.Game.Skinning { public class LegacySkinResourceStore : ResourceStore { - private readonly IHasRealmFiles source; + private readonly IHasNamedFiles source; - public LegacySkinResourceStore(IHasRealmFiles source, IResourceStore underlyingStore) + public LegacySkinResourceStore(IHasNamedFiles source, IResourceStore underlyingStore) : base(underlyingStore) { this.source = source; @@ -32,7 +32,7 @@ namespace osu.Game.Skinning } private string getPathForFile(string filename) => - source.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.FileInfo.GetStoragePath(); + source.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath(); public override IEnumerable GetAvailableResources() => source.Files.Select(f => f.Filename); } From 29d074bdb8629ff480207498c0eeec61b010a530 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 18:07:32 +0900 Subject: [PATCH 046/419] Implement missing behaviours required for skin file operations via `RealmArchiveModelManager` --- osu.Game/OsuGameBase.cs | 10 +- osu.Game/Skinning/SkinManager.cs | 163 ++++++++----------- osu.Game/Skinning/SkinModelManager.cs | 144 ++++++++++------ osu.Game/Stores/RealmArchiveModelManager.cs | 172 ++++++++++++++++++++ 4 files changed, 332 insertions(+), 157 deletions(-) create mode 100644 osu.Game/Stores/RealmArchiveModelManager.cs diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 88c9ab370c..4bae6f3c1d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -211,17 +211,9 @@ namespace osu.Game Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY; - dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Resources, Audio)); + dependencies.Cache(SkinManager = new SkinManager(Storage, realmFactory, Host, Resources, Audio, Scheduler)); dependencies.CacheAs(SkinManager); - // needs to be done here rather than inside SkinManager to ensure thread safety of CurrentSkinInfo. - SkinManager.ItemRemoved += item => Schedule(() => - { - // check the removed skin is not the current user choice. if it is, switch back to default. - if (item.Equals(SkinManager.CurrentSkinInfo.Value)) - SkinManager.CurrentSkinInfo.Value = SkinInfo.Default; - }); - EndpointConfiguration endpoints = UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration(); MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl; diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 24ec454276..2ec934ac32 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -3,14 +3,10 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Linq.Expressions; -using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; @@ -20,6 +16,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Framework.Testing; +using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Database; @@ -37,7 +34,7 @@ namespace osu.Game.Skinning /// For gameplay components, see which adds extra legacy and toggle logic that may affect the lookup process. /// [ExcludeFromDynamicCompile] - public class SkinManager : ISkinSource, IStorageResourceProvider, IModelImporter, IModelManager + public class SkinManager : ISkinSource, IStorageResourceProvider, IModelImporter { private readonly AudioManager audio; @@ -49,8 +46,7 @@ namespace osu.Game.Skinning public readonly Bindable> CurrentSkinInfo = new Bindable>(SkinInfo.Default.ToLive()) { Default = SkinInfo.Default.ToLive() }; private readonly SkinModelManager skinModelManager; - - private readonly SkinStore skinStore; + private readonly RealmContextFactory contextFactory; private readonly IResourceStore userFiles; @@ -64,69 +60,73 @@ namespace osu.Game.Skinning /// public Skin DefaultLegacySkin { get; } - public SkinManager(Storage storage, DatabaseContextFactory contextFactory, GameHost host, IResourceStore resources, AudioManager audio) + public SkinManager(Storage storage, RealmContextFactory contextFactory, GameHost host, IResourceStore resources, AudioManager audio, Scheduler scheduler) { + this.contextFactory = contextFactory; this.audio = audio; this.host = host; this.resources = resources; - skinStore = new SkinStore(contextFactory, storage); - userFiles = new FileStore(contextFactory, storage).Store; + userFiles = new StorageBackedResourceStore(storage.GetStorageForDirectory("files")); - skinModelManager = new SkinModelManager(storage, contextFactory, skinStore, host, this); + skinModelManager = new SkinModelManager(storage, contextFactory, host, this); DefaultLegacySkin = new DefaultLegacySkin(this); DefaultSkin = new DefaultSkin(this); - CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue); + CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin); CurrentSkin.Value = DefaultSkin; CurrentSkin.ValueChanged += skin => { - if (skin.NewValue.SkinInfo != CurrentSkinInfo.Value) + if (!skin.NewValue.SkinInfo.Equals(CurrentSkinInfo.Value)) throw new InvalidOperationException($"Setting {nameof(CurrentSkin)}'s value directly is not supported. Use {nameof(CurrentSkinInfo)} instead."); SourceChanged?.Invoke(); }; + + // needs to be done here rather than inside SkinManager to ensure thread safety of CurrentSkinInfo. + ItemRemoved += item => scheduler.Add(() => + { + // TODO: fix. + // check the removed skin is not the current user choice. if it is, switch back to default. + // if (item.Equals(CurrentSkinInfo.Value)) + // CurrentSkinInfo.Value = SkinInfo.Default; + }); } /// - /// Returns a list of all usable s. Includes the special default skin plus all skins from . + /// Returns a list of all usable s. Includes the non-databased default skins. /// /// A newly allocated list of available . - public List GetAllUsableSkins() + public List> GetAllUsableSkins() { - var userSkins = GetAllUserSkins(); - userSkins.Insert(0, DefaultSkin.SkinInfo); - userSkins.Insert(1, DefaultLegacySkin.SkinInfo); - return userSkins; - } - - /// - /// Returns a list of all usable s that have been loaded by the user. - /// - /// A newly allocated list of available . - public List GetAllUserSkins(bool includeFiles = false) - { - if (includeFiles) - return skinStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); - - return skinStore.Items.Where(s => !s.DeletePending).ToList(); + using (var context = contextFactory.CreateContext()) + { + var userSkins = context.All().Where(s => !s.DeletePending).ToLive(); + userSkins.Insert(0, DefaultSkin.SkinInfo); + userSkins.Insert(1, DefaultLegacySkin.SkinInfo); + return userSkins; + } } public void SelectRandomSkin() { - // choose from only user skins, removing the current selection to ensure a new one is chosen. - var randomChoices = skinStore.Items.Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray(); - - if (randomChoices.Length == 0) + using (var context = contextFactory.CreateContext()) { - CurrentSkinInfo.Value = SkinInfo.Default; - return; - } + // choose from only user skins, removing the current selection to ensure a new one is chosen. + var randomChoices = context.All().Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray(); - var chosen = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length)); - CurrentSkinInfo.Value = skinStore.ConsumableItems.Single(i => i.ID == chosen.ID); + if (randomChoices.Length == 0) + { + CurrentSkinInfo.Value = SkinInfo.Default.ToLive(); + return; + } + + var chosen = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length)); + + CurrentSkinInfo.Value = chosen.ToLive(); + } } /// @@ -142,40 +142,30 @@ namespace osu.Game.Skinning /// public void EnsureMutableSkin() { - if (CurrentSkinInfo.Value.ID >= 1) return; - - var skin = CurrentSkin.Value; - - // if the user is attempting to save one of the default skin implementations, create a copy first. - CurrentSkinInfo.Value = skinModelManager.Import(new SkinInfo + CurrentSkinInfo.Value.PerformRead(s => { - Name = skin.SkinInfo.Name + @" (modified)", - Creator = skin.SkinInfo.Creator, - InstantiationInfo = skin.SkinInfo.InstantiationInfo, - }).Result.Value; + if (s.IsManaged) + return; + + // if the user is attempting to save one of the default skin implementations, create a copy first. + var result = skinModelManager.Import(new SkinInfo + { + Name = s.Name + @" (modified)", + Creator = s.Creator, + InstantiationInfo = s.InstantiationInfo, + }).Result; + + if (result != null) + CurrentSkinInfo.Value = result; + }); } public void Save(Skin skin) { - if (skin.SkinInfo.ID <= 0) + if (!skin.SkinInfo.IsManaged) throw new InvalidOperationException($"Attempting to save a skin which is not yet tracked. Call {nameof(EnsureMutableSkin)} first."); - foreach (var drawableInfo in skin.DrawableComponentInfo) - { - string json = JsonConvert.SerializeObject(drawableInfo.Value, new JsonSerializerSettings { Formatting = Formatting.Indented }); - - using (var streamContent = new MemoryStream(Encoding.UTF8.GetBytes(json))) - { - string filename = @$"{drawableInfo.Key}.json"; - - var oldFile = skin.SkinInfo.Files.FirstOrDefault(f => f.Filename == filename); - - if (oldFile != null) - skinModelManager.ReplaceFile(skin.SkinInfo, oldFile, streamContent); - else - skinModelManager.AddFile(skin.SkinInfo, streamContent, filename); - } - } + skinModelManager.Save(skin); } /// @@ -183,7 +173,11 @@ namespace osu.Game.Skinning /// /// The query. /// The first result for the provided query, or null if no results were found. - public SkinInfo Query(Expression> query) => skinStore.ConsumableItems.AsNoTracking().FirstOrDefault(query); + public ILive Query(Expression> query) + { + using (var context = contextFactory.CreateContext()) + return context.All().FirstOrDefault(query)?.ToLive(); + } public event Action SourceChanged; @@ -301,34 +295,13 @@ namespace osu.Game.Skinning remove => skinModelManager.ItemRemoved -= value; } - public void Update(SkinInfo item) + public void Delete(Expression> filter, bool silent = false) { - skinModelManager.Update(item); - } - - public bool Delete(SkinInfo item) - { - return skinModelManager.Delete(item); - } - - public void Delete(List items, bool silent = false) - { - skinModelManager.Delete(items, silent); - } - - public void Undelete(List items, bool silent = false) - { - skinModelManager.Undelete(items, silent); - } - - public void Undelete(SkinInfo item) - { - skinModelManager.Undelete(item); - } - - public bool IsAvailableLocally(SkinInfo model) - { - return skinModelManager.IsAvailableLocally(model); + using (var context = contextFactory.CreateContext()) + { + var items = context.All().Where(filter).ToList(); + skinModelManager.Delete(items, silent); + } } #endregion diff --git a/osu.Game/Skinning/SkinModelManager.cs b/osu.Game/Skinning/SkinModelManager.cs index 572ae5cbfc..059345f9bc 100644 --- a/osu.Game/Skinning/SkinModelManager.cs +++ b/osu.Game/Skinning/SkinModelManager.cs @@ -8,21 +8,26 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using Newtonsoft.Json; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Extensions; using osu.Game.IO; using osu.Game.IO.Archives; +using osu.Game.Stores; +using Realms; + +#nullable enable namespace osu.Game.Skinning { - public class SkinModelManager : ArchiveModelManager + public class SkinModelManager : RealmArchiveModelManager { private readonly IStorageResourceProvider skinResources; - public SkinModelManager(Storage storage, DatabaseContextFactory contextFactory, SkinStore skinStore, GameHost host, IStorageResourceProvider skinResources) - : base(storage, contextFactory, skinStore, host) + public SkinModelManager(Storage storage, RealmContextFactory contextFactory, GameHost host, IStorageResourceProvider skinResources) + : base(storage, contextFactory) { this.skinResources = skinResources; @@ -42,18 +47,27 @@ namespace osu.Game.Skinning protected override bool HasCustomHashFunction => true; - protected override string ComputeHash(SkinInfo item) + protected override Task Populate(SkinInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(model.InstantiationInfo)) + model.InstantiationInfo = createInstance(model).GetType().GetInvariantInstantiationInfo(); + + checkSkinIniMetadata(model, realm); + + return Task.CompletedTask; + } + + private void checkSkinIniMetadata(SkinInfo item, Realm realm) { var instance = createInstance(item); // This function can be run on fresh import or save. The logic here ensures a skin.ini file is in a good state for both operations. - // `Skin` will parse the skin.ini and populate `Skin.Configuration` during construction above. string skinIniSourcedName = instance.Configuration.SkinInfo.Name; string skinIniSourcedCreator = instance.Configuration.SkinInfo.Creator; string archiveName = item.Name.Replace(@".osk", string.Empty, StringComparison.OrdinalIgnoreCase); - bool isImport = item.ID == 0; + bool isImport = !item.IsManaged; if (isImport) { @@ -71,12 +85,10 @@ namespace osu.Game.Skinning // Regardless of whether this is an import or not, let's write the skin.ini if non-existing or non-matching. // This is (weirdly) done inside ComputeHash to avoid adding a new method to handle this case. After switching to realm it can be moved into another place. if (skinIniSourcedName != item.Name) - updateSkinIniMetadata(item); - - return base.ComputeHash(item); + updateSkinIniMetadata(item, realm); } - private void updateSkinIniMetadata(SkinInfo item) + private void updateSkinIniMetadata(SkinInfo item, Realm realm) { string nameLine = @$"Name: {item.Name}"; string authorLine = @$"Author: {item.Creator}"; @@ -95,39 +107,47 @@ namespace osu.Game.Skinning { // In the case a skin doesn't have a skin.ini yet, let's create one. writeNewSkinIni(); - return; } - - using (Stream stream = new MemoryStream()) + else { - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + using (Stream stream = new MemoryStream()) { - using (var existingStream = Files.Storage.GetStream(existingFile.FileInfo.GetStoragePath())) - using (var sr = new StreamReader(existingStream)) + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) { - string line; - while ((line = sr.ReadLine()) != null) + using (var existingStream = Files.Storage.GetStream(existingFile.File.GetStoragePath())) + using (var sr = new StreamReader(existingStream)) + { + string? line; + while ((line = sr.ReadLine()) != null) + sw.WriteLine(line); + } + + sw.WriteLine(); + + foreach (string line in newLines) sw.WriteLine(line); } - sw.WriteLine(); + ReplaceFile(item, existingFile, stream, realm); - foreach (string line in newLines) - sw.WriteLine(line); - } + // can be removed 20220502. + if (!ensureIniWasUpdated(item)) + { + Logger.Log($"Skin {item}'s skin.ini had issues and has been removed. Please report this and provide the problematic skin.", LoggingTarget.Database, LogLevel.Important); - ReplaceFile(item, existingFile, stream); + var existingIni = item.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase)); + if (existingIni != null) + item.Files.Remove(existingIni); - // can be removed 20220502. - if (!ensureIniWasUpdated(item)) - { - Logger.Log($"Skin {item}'s skin.ini had issues and has been removed. Please report this and provide the problematic skin.", LoggingTarget.Database, LogLevel.Important); - - DeleteFile(item, item.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase))); - writeNewSkinIni(); + writeNewSkinIni(); + } } } + // The hash is already populated at this point in import. + // As we have changed files, it needs to be recomputed. + item.Hash = ComputeHash(item); + void writeNewSkinIni() { using (Stream stream = new MemoryStream()) @@ -138,8 +158,10 @@ namespace osu.Game.Skinning sw.WriteLine(line); } - AddFile(item, stream, @"skin.ini"); + AddFile(item, stream, @"skin.ini", realm); } + + item.Hash = ComputeHash(item); } } @@ -154,36 +176,52 @@ namespace osu.Game.Skinning return instance.Configuration.SkinInfo.Name == item.Name; } - protected override Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) - { - var instance = createInstance(model); - - model.InstantiationInfo ??= instance.GetType().GetInvariantInstantiationInfo(); - - model.Name = instance.Configuration.SkinInfo.Name; - model.Creator = instance.Configuration.SkinInfo.Creator; - - return Task.CompletedTask; - } - private void populateMissingHashes() { - var skinsWithoutHashes = ModelStore.ConsumableItems.Where(i => i.Hash == null).ToArray(); - - foreach (SkinInfo skin in skinsWithoutHashes) + using (var realm = ContextFactory.CreateContext()) { - try + var skinsWithoutHashes = realm.All().Where(i => string.IsNullOrEmpty(i.Hash)).ToArray(); + + foreach (SkinInfo skin in skinsWithoutHashes) { - Update(skin); - } - catch (Exception e) - { - Delete(skin); - Logger.Error(e, $"Existing skin {skin} has been deleted during hash recomputation due to being invalid"); + try + { + Update(skin); + } + catch (Exception e) + { + Delete(skin); + Logger.Error(e, $"Existing skin {skin} has been deleted during hash recomputation due to being invalid"); + } } } } private Skin createInstance(SkinInfo item) => item.CreateInstance(skinResources); + + public void Save(Skin skin) + { + skin.SkinInfo.PerformWrite(s => + { + foreach (var drawableInfo in skin.DrawableComponentInfo) + { + string json = JsonConvert.SerializeObject(drawableInfo.Value, new JsonSerializerSettings { Formatting = Formatting.Indented }); + + using (var streamContent = new MemoryStream(Encoding.UTF8.GetBytes(json))) + { + string filename = @$"{drawableInfo.Key}.json"; + + var oldFile = s.Files.FirstOrDefault(f => f.Filename == filename); + + if (oldFile != null) + ReplaceFile(s, oldFile, streamContent, s.Realm); + else + AddFile(s, streamContent, filename, s.Realm); + } + } + + s.Hash = ComputeHash(s); + }); + } } } diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs new file mode 100644 index 0000000000..14b7077dd2 --- /dev/null +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -0,0 +1,172 @@ +// 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.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Platform; +using osu.Game.Database; +using osu.Game.IO.Archives; +using osu.Game.Models; +using osu.Game.Overlays.Notifications; +using Realms; + +#nullable enable + +namespace osu.Game.Stores +{ + /// + /// Class which adds all the missing pieces bridging the gap between and . + /// + public abstract class RealmArchiveModelManager : RealmArchiveModelImporter, IModelManager, IModelFileManager + where TModel : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey, ISoftDelete + { + public event Action? ItemUpdated; + public event Action? ItemRemoved; + + private readonly RealmFileStore realmFileStore; + + protected RealmArchiveModelManager(Storage storage, RealmContextFactory contextFactory) + : base(storage, contextFactory) + { + realmFileStore = new RealmFileStore(contextFactory, storage); + } + + public void DeleteFile(TModel item, RealmNamedFileUsage file) => + item.Realm.Write(() => DeleteFile(item, file, item.Realm)); + + public void ReplaceFile(TModel item, RealmNamedFileUsage file, Stream contents) + => item.Realm.Write(() => ReplaceFile(item, file, contents, item.Realm)); + + public void AddFile(TModel item, Stream stream, string filename) + => item.Realm.Write(() => AddFile(item, stream, filename, item.Realm)); + + public void DeleteFile(TModel item, RealmNamedFileUsage file, Realm realm) + { + item.Files.Remove(file); + } + + public void ReplaceFile(TModel model, RealmNamedFileUsage file, Stream contents, Realm realm) + { + file.File = realmFileStore.Add(contents, realm); + } + + public void AddFile(TModel item, Stream stream, string filename, Realm realm) + { + var file = realmFileStore.Add(stream, realm); + var namedUsage = new RealmNamedFileUsage(file, filename); + + item.Files.Add(namedUsage); + } + + public override async Task?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + { + var imported = await base.Import(item, archive, lowPriority, cancellationToken).ConfigureAwait(false); + + imported?.PerformRead(i => ItemUpdated?.Invoke(i.Detach())); + + return imported; + } + + /// + /// Delete multiple items. + /// This will post notifications tracking progress. + /// + public void Delete(List items, bool silent = false) + { + if (items.Count == 0) return; + + var notification = new ProgressNotification + { + Progress = 0, + Text = $"Preparing to delete all {HumanisedModelName}s...", + CompletionText = $"Deleted all {HumanisedModelName}s!", + State = ProgressNotificationState.Active, + }; + + if (!silent) + PostNotification?.Invoke(notification); + + int i = 0; + + foreach (var b in items) + { + if (notification.State == ProgressNotificationState.Cancelled) + // user requested abort + return; + + notification.Text = $"Deleting {HumanisedModelName}s ({++i} of {items.Count})"; + + Delete(b); + + notification.Progress = (float)i / items.Count; + } + + notification.State = ProgressNotificationState.Completed; + } + + /// + /// Restore multiple items that were previously deleted. + /// This will post notifications tracking progress. + /// + public void Undelete(List items, bool silent = false) + { + if (!items.Any()) return; + + var notification = new ProgressNotification + { + CompletionText = "Restored all deleted items!", + Progress = 0, + State = ProgressNotificationState.Active, + }; + + if (!silent) + PostNotification?.Invoke(notification); + + int i = 0; + + foreach (var item in items) + { + if (notification.State == ProgressNotificationState.Cancelled) + // user requested abort + return; + + notification.Text = $"Restoring ({++i} of {items.Count})"; + + Undelete(item); + + notification.Progress = (float)i / items.Count; + } + + notification.State = ProgressNotificationState.Completed; + } + + public bool Delete(TModel item) + { + if (item.DeletePending) + return false; + + item.Realm.Write(r => item.DeletePending = true); + ItemRemoved?.Invoke(item.Detach()); + return true; + } + + public void Undelete(TModel item) + { + if (!item.DeletePending) + return; + + item.Realm.Write(r => item.DeletePending = false); + ItemUpdated?.Invoke(item); + } + + public virtual bool IsAvailableLocally(TModel model) => false; // TODO: implement + + public void Update(TModel skin) + { + } + } +} From c629a7a36fe1acdd9e1143fb9c6692b5152c38a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 16:18:34 +0900 Subject: [PATCH 047/419] Fix random selection and avoid using legacy events for handling skin import/deletion --- .../Overlays/Settings/Sections/SkinSection.cs | 95 +++++++++---------- osu.Game/Skinning/SkinManager.cs | 33 +++---- osu.Game/Stores/RealmArchiveModelManager.cs | 27 ++++-- 3 files changed, 73 insertions(+), 82 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 9c5a2dff47..88f60a6004 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -18,6 +18,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Skinning; using osu.Game.Skinning.Editor; +using Realms; namespace osu.Game.Overlays.Settings.Sections { @@ -43,21 +44,15 @@ namespace osu.Game.Overlays.Settings.Sections private List> skinItems; - private int firstNonDefaultSkinIndex - { - get - { - int index = skinItems.FindIndex(s => s.ID == SkinInfo.CLASSIC_SKIN); - if (index < 0) - index = skinItems.Count; - - return index; - } - } - [Resolved] private SkinManager skins { get; set; } + [Resolved] + private RealmContextFactory realmFactory { get; set; } + + private IDisposable realmSubscription; + private IQueryable realmSkins; + [BackgroundDependencyLoader(permitNulls: true)] private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor) { @@ -75,9 +70,6 @@ namespace osu.Game.Overlays.Settings.Sections new ExportSkinButton(), }; - skins.ItemUpdated += itemUpdated; - skins.ItemRemoved += itemRemoved; - config.BindWith(OsuSetting.Skin, configBindable); } @@ -86,6 +78,21 @@ namespace osu.Game.Overlays.Settings.Sections base.LoadComplete(); skinDropdown.Current = dropdownBindable; + + realmSkins = realmFactory.Context.All() + .Where(s => !s.DeletePending) + .OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase); + + realmSubscription = realmSkins + .SubscribeForNotifications((sender, changes, error) => + { + if (changes == null) + return; + + // Eventually this should be handling the individual changes rather than refreshing the whole dropdown. + updateItems(); + }); + updateItems(); // Todo: This should not be necessary when OsuConfigManager is databased @@ -97,7 +104,16 @@ namespace osu.Game.Overlays.Settings.Sections { if (skin.NewValue.Equals(random_skin_info)) { + var skinBefore = skins.CurrentSkinInfo.Value; + skins.SelectRandomSkin(); + + if (skinBefore == skins.CurrentSkinInfo.Value) + { + // the random selection didn't change the skin, so we should manually update the dropdown to match. + dropdownBindable.Value = skins.CurrentSkinInfo.Value; + } + return; } @@ -111,12 +127,13 @@ namespace osu.Game.Overlays.Settings.Sections var skin = skinDropdown.Items.FirstOrDefault(s => s.ID == configId); + // TODO: i don't think this will be required any more. if (skin == null) { // there may be a thread race condition where an item is selected that hasn't yet been added to the dropdown. // to avoid adding complexity, let's just ensure the item is added so we can perform the selection. skin = skins.Query(s => s.ID == configId); - addItem(skin); + updateItems(); } dropdownBindable.Value = skin; @@ -124,47 +141,20 @@ namespace osu.Game.Overlays.Settings.Sections private void updateItems() { - skinItems = skins.GetAllUsableSkins(); - skinItems.Insert(firstNonDefaultSkinIndex, random_skin_info); - sortUserSkins(skinItems); + skinItems = realmSkins.ToLive(); + + skinItems.Insert(0, SkinInfo.Default.ToLive()); + skinItems.Insert(1, DefaultLegacySkin.Info.ToLive()); + skinItems.Insert(2, random_skin_info); + skinDropdown.Items = skinItems; } - private void itemUpdated(SkinInfo item) => Schedule(() => addItem(item.ToLive())); - - private void addItem(ILive item) - { - List> newDropdownItems = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToList(); - sortUserSkins(newDropdownItems); - skinDropdown.Items = newDropdownItems; - } - - private void itemRemoved(SkinInfo item) => Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => !i.ID.Equals(item.ID)).ToArray()); - - private void sortUserSkins(List> skinsList) - { - try - { - // Sort user skins separately from built-in skins - skinsList.Sort(firstNonDefaultSkinIndex, skinsList.Count - firstNonDefaultSkinIndex, - Comparer>.Create((a, b) => - { - // o_________________________o - return a.PerformRead(ai => b.PerformRead(bi => string.Compare(ai.Name, bi.Name, StringComparison.OrdinalIgnoreCase))); - })); - } - catch { } - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - if (skins != null) - { - skins.ItemUpdated -= itemUpdated; - skins.ItemRemoved -= itemRemoved; - } + realmSubscription?.Dispose(); } private class SkinSettingsDropdown : SettingsDropdown> @@ -192,6 +182,11 @@ namespace osu.Game.Overlays.Settings.Sections { Text = SkinSettingsStrings.ExportSkinButton; Action = export; + } + + protected override void LoadComplete() + { + base.LoadComplete(); currentSkin = skins.CurrentSkin.GetBoundCopy(); currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.PerformRead(s => s.IsManaged), true); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 2ec934ac32..bcad5277ab 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -38,6 +38,8 @@ namespace osu.Game.Skinning { private readonly AudioManager audio; + private readonly Scheduler scheduler; + private readonly GameHost host; private readonly IResourceStore resources; @@ -64,6 +66,7 @@ namespace osu.Game.Skinning { this.contextFactory = contextFactory; this.audio = audio; + this.scheduler = scheduler; this.host = host; this.resources = resources; @@ -84,15 +87,6 @@ namespace osu.Game.Skinning SourceChanged?.Invoke(); }; - - // needs to be done here rather than inside SkinManager to ensure thread safety of CurrentSkinInfo. - ItemRemoved += item => scheduler.Add(() => - { - // TODO: fix. - // check the removed skin is not the current user choice. if it is, switch back to default. - // if (item.Equals(CurrentSkinInfo.Value)) - // CurrentSkinInfo.Value = SkinInfo.Default; - }); } /// @@ -283,23 +277,18 @@ namespace osu.Game.Skinning #region Implementation of IModelManager - public event Action ItemUpdated - { - add => skinModelManager.ItemUpdated += value; - remove => skinModelManager.ItemUpdated -= value; - } - - public event Action ItemRemoved - { - add => skinModelManager.ItemRemoved += value; - remove => skinModelManager.ItemRemoved -= value; - } - - public void Delete(Expression> filter, bool silent = false) + public void Delete([CanBeNull] Expression> filter = null, bool silent = false) { using (var context = contextFactory.CreateContext()) { var items = context.All().Where(filter).ToList(); + + // check the removed skin is not the current user choice. if it is, switch back to default. + Guid currentUserSkin = CurrentSkinInfo.Value.ID; + + if (items.Any(s => s.ID == currentUserSkin)) + scheduler.Add(() => CurrentSkinInfo.Value = SkinInfo.Default.ToLive()); + skinModelManager.Delete(items, silent); } } diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index 14b7077dd2..ae40bcac79 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -24,8 +24,21 @@ namespace osu.Game.Stores public abstract class RealmArchiveModelManager : RealmArchiveModelImporter, IModelManager, IModelFileManager where TModel : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey, ISoftDelete { - public event Action? ItemUpdated; - public event Action? ItemRemoved; + public event Action? ItemUpdated + { + // This may be brought back for beatmaps to ease integration. + // The eventual goal would be not requiring this and using realm subscriptions in its place. + add => throw new NotImplementedException(); + remove => throw new NotImplementedException(); + } + + public event Action? ItemRemoved + { + // This may be brought back for beatmaps to ease integration. + // The eventual goal would be not requiring this and using realm subscriptions in its place. + add => throw new NotImplementedException(); + remove => throw new NotImplementedException(); + } private readonly RealmFileStore realmFileStore; @@ -64,11 +77,7 @@ namespace osu.Game.Stores public override async Task?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { - var imported = await base.Import(item, archive, lowPriority, cancellationToken).ConfigureAwait(false); - - imported?.PerformRead(i => ItemUpdated?.Invoke(i.Detach())); - - return imported; + return await base.Import(item, archive, lowPriority, cancellationToken).ConfigureAwait(false); } /// @@ -150,7 +159,6 @@ namespace osu.Game.Stores return false; item.Realm.Write(r => item.DeletePending = true); - ItemRemoved?.Invoke(item.Detach()); return true; } @@ -160,10 +168,9 @@ namespace osu.Game.Stores return; item.Realm.Write(r => item.DeletePending = false); - ItemUpdated?.Invoke(item); } - public virtual bool IsAvailableLocally(TModel model) => false; // TODO: implement + public virtual bool IsAvailableLocally(TModel model) => false; // Not relevant for skins since they can't be downloaded yet. public void Update(TModel skin) { From f6a3709060b3ba4d81ce4f9d01df52aba7fa18dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 17:15:26 +0900 Subject: [PATCH 048/419] Store default skins to database --- .../TestSceneHyperDashColouring.cs | 2 +- .../Editor/TestSceneManiaComposeScreen.cs | 4 +- .../TestSceneSliderApplication.cs | 2 +- .../Gameplay/TestSceneStoryboardSamples.cs | 2 +- .../Gameplay/TestSceneBeatmapSkinFallbacks.cs | 2 +- osu.Game/OsuGame.cs | 6 +-- .../Overlays/Settings/Sections/SkinSection.cs | 11 ++--- osu.Game/Skinning/DefaultLegacySkin.cs | 19 ++++---- osu.Game/Skinning/DefaultSkin.cs | 11 ++++- osu.Game/Skinning/SkinInfo.cs | 11 +---- osu.Game/Skinning/SkinManager.cs | 43 +++++++++++-------- 11 files changed, 61 insertions(+), 52 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index 70b2c8c82a..14a4d02396 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Catch.Tests private Drawable setupSkinHierarchy(Drawable child, ISkin skin) { - var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info)); + var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.CreateInfo())); var testSkinProvider = new SkinProvidingContainer(skin); var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider)); diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs index a7e76e44b5..91f5f93905 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs @@ -56,13 +56,13 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor [Test] public void TestDefaultSkin() { - AddStep("set default skin", () => skins.CurrentSkinInfo.Value = SkinInfo.Default.ToLive()); + AddStep("set default skin", () => skins.CurrentSkinInfo.Value = DefaultSkin.CreateInfo().ToLive()); } [Test] public void TestLegacySkin() { - AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info.ToLive()); + AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.CreateInfo().ToLive()); } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs index e698766aac..d673b7a6ac 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("create slider", () => { - var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.Info); + var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo()); tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1"; Child = new SkinProvidingContainer(tintingSkin) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 3bf6aaac7a..88f35976ad 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -167,7 +167,7 @@ namespace osu.Game.Tests.Gameplay private class TestSkin : LegacySkin { public TestSkin(string resourceName, IStorageResourceProvider resources) - : base(DefaultLegacySkin.Info, new TestResourceStore(resourceName), resources, "skin.ini") + : base(DefaultLegacySkin.CreateInfo(), new TestResourceStore(resourceName), resources, "skin.ini") { } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index c174a3edc2..cccc962a3f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestEmptyLegacyBeatmapSkinFallsBack() { - CreateSkinTest(SkinInfo.Default, () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null)); + CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null)); AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded)); AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value)); } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1ffbb4cb85..8af76c1289 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -256,10 +256,10 @@ namespace osu.Game if (skinInfo == null) { if (guid == SkinInfo.CLASSIC_SKIN) - skinInfo = DefaultLegacySkin.Info.ToLive(); + skinInfo = DefaultLegacySkin.CreateInfo().ToLive(); } - SkinManager.CurrentSkinInfo.Value = skinInfo ?? SkinInfo.Default.ToLive(); + SkinManager.CurrentSkinInfo.Value = skinInfo ?? DefaultSkin.CreateInfo().ToLive(); }; configSkin.TriggerChange(); @@ -660,7 +660,7 @@ namespace osu.Game // make config aware of how to lookup skins for on-screen display purposes. // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. - LocalConfig.LookupSkinName = id => SkinManager.GetAllUsableSkins().FirstOrDefault(s => s.ID == id)?.ToString() ?? "Unknown"; + LocalConfig.LookupSkinName = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown"; LocalConfig.LookupKeyBindings = l => { diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 88f60a6004..bf0f6bf142 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Settings.Sections Icon = FontAwesome.Solid.PaintBrush }; - private readonly Bindable> dropdownBindable = new Bindable> { Default = SkinInfo.Default.ToLive() }; + private readonly Bindable> dropdownBindable = new Bindable> { Default = DefaultSkin.CreateInfo().ToLive() }; private readonly Bindable configBindable = new Bindable(); private static readonly ILive random_skin_info = new SkinInfo @@ -81,7 +81,8 @@ namespace osu.Game.Overlays.Settings.Sections realmSkins = realmFactory.Context.All() .Where(s => !s.DeletePending) - .OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase); + .OrderBy(s => s.Protected) + .ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase); realmSubscription = realmSkins .SubscribeForNotifications((sender, changes, error) => @@ -141,11 +142,11 @@ namespace osu.Game.Overlays.Settings.Sections private void updateItems() { + int protectedCount = realmSkins.Count(s => s.Protected); + skinItems = realmSkins.ToLive(); - skinItems.Insert(0, SkinInfo.Default.ToLive()); - skinItems.Insert(1, DefaultLegacySkin.Info.ToLive()); - skinItems.Insert(2, random_skin_info); + skinItems.Insert(protectedCount, random_skin_info); skinDropdown.Items = skinItems; } diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index 2332e6e160..c7033d37dc 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -12,8 +12,17 @@ namespace osu.Game.Skinning { public class DefaultLegacySkin : LegacySkin { + public static SkinInfo CreateInfo() => new SkinInfo + { + ID = Skinning.SkinInfo.CLASSIC_SKIN, // this is temporary until database storage is decided upon. + Name = "osu!classic", + Creator = "team osu!", + Protected = true, + InstantiationInfo = typeof(DefaultLegacySkin).GetInvariantInstantiationInfo() + }; + public DefaultLegacySkin(IStorageResourceProvider resources) - : this(Info, resources) + : this(CreateInfo(), resources) { } @@ -39,13 +48,5 @@ namespace osu.Game.Skinning Configuration.LegacyVersion = 2.7m; } - - public static SkinInfo Info { get; } = new SkinInfo - { - ID = osu.Game.Skinning.SkinInfo.CLASSIC_SKIN, // this is temporary until database storage is decided upon. - Name = "osu!classic", - Creator = "team osu!", - InstantiationInfo = typeof(DefaultLegacySkin).GetInvariantInstantiationInfo() - }; } } diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 8d3a83f589..951e3f9cc5 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -23,10 +23,19 @@ namespace osu.Game.Skinning { public class DefaultSkin : Skin { + public static SkinInfo CreateInfo() => new SkinInfo + { + ID = osu.Game.Skinning.SkinInfo.DEFAULT_SKIN, + Name = "osu! (triangles)", + Creator = "team osu!", + Protected = true, + InstantiationInfo = typeof(DefaultSkin).GetInvariantInstantiationInfo() + }; + private readonly IStorageResourceProvider resources; public DefaultSkin(IStorageResourceProvider resources) - : this(osu.Game.Skinning.SkinInfo.Default, resources) + : this(CreateInfo(), resources) { } diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index c09d45c227..9a82964933 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Testing; using osu.Game.Database; -using osu.Game.Extensions; using osu.Game.IO; using osu.Game.Models; using Realms; @@ -32,6 +31,8 @@ namespace osu.Game.Skinning public string Hash { get; set; } = string.Empty; + public bool Protected { get; set; } + public string InstantiationInfo { get; set; } = string.Empty; public virtual Skin CreateInstance(IStorageResourceProvider resources) @@ -48,14 +49,6 @@ namespace osu.Game.Skinning public bool DeletePending { get; set; } - public static SkinInfo Default { get; } = new SkinInfo - { - ID = DEFAULT_SKIN, - Name = "osu! (triangles)", - Creator = "team osu!", - InstantiationInfo = typeof(DefaultSkin).GetInvariantInstantiationInfo() - }; - public bool Equals(SkinInfo? other) { if (ReferenceEquals(this, other)) return true; diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index bcad5277ab..5c07244e0f 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -45,7 +45,11 @@ namespace osu.Game.Skinning private readonly IResourceStore resources; public readonly Bindable CurrentSkin = new Bindable(); - public readonly Bindable> CurrentSkinInfo = new Bindable>(SkinInfo.Default.ToLive()) { Default = SkinInfo.Default.ToLive() }; + + public readonly Bindable> CurrentSkinInfo = new Bindable>(Skinning.DefaultSkin.CreateInfo().ToLive()) + { + Default = Skinning.DefaultSkin.CreateInfo().ToLive() + }; private readonly SkinModelManager skinModelManager; private readonly RealmContextFactory contextFactory; @@ -74,8 +78,24 @@ namespace osu.Game.Skinning skinModelManager = new SkinModelManager(storage, contextFactory, host, this); - DefaultLegacySkin = new DefaultLegacySkin(this); - DefaultSkin = new DefaultSkin(this); + var defaultSkins = new[] + { + DefaultLegacySkin = new DefaultLegacySkin(this), + DefaultSkin = new DefaultSkin(this), + }; + + // Ensure the default entries are present. + using (var context = contextFactory.CreateContext()) + using (var transaction = context.BeginWrite()) + { + foreach (var skin in defaultSkins) + { + if (context.Find(skin.SkinInfo.ID) == null) + context.Add(skin.SkinInfo.Value); + } + + transaction.Commit(); + } CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin); @@ -89,21 +109,6 @@ namespace osu.Game.Skinning }; } - /// - /// Returns a list of all usable s. Includes the non-databased default skins. - /// - /// A newly allocated list of available . - public List> GetAllUsableSkins() - { - using (var context = contextFactory.CreateContext()) - { - var userSkins = context.All().Where(s => !s.DeletePending).ToLive(); - userSkins.Insert(0, DefaultSkin.SkinInfo); - userSkins.Insert(1, DefaultLegacySkin.SkinInfo); - return userSkins; - } - } - public void SelectRandomSkin() { using (var context = contextFactory.CreateContext()) @@ -113,7 +118,7 @@ namespace osu.Game.Skinning if (randomChoices.Length == 0) { - CurrentSkinInfo.Value = SkinInfo.Default.ToLive(); + CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLive(); return; } From 0d18c83d758e033daf823218015e25eb1458960f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 17:15:48 +0900 Subject: [PATCH 049/419] Simplify deletion by adding always present conditionals to `Delete` method --- .../Settings/Sections/Maintenance/GeneralSettings.cs | 2 +- osu.Game/Skinning/SkinManager.cs | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index e7e196555f..98ccbf85fd 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -108,7 +108,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance deleteSkinsButton.Enabled.Value = false; Task.Run(() => { - skins.Delete(s => !s.DeletePending); + skins.Delete(); }).ContinueWith(t => Schedule(() => deleteSkinsButton.Enabled.Value = true)); })); } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 5c07244e0f..d6c884d259 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; @@ -286,15 +287,18 @@ namespace osu.Game.Skinning { using (var context = contextFactory.CreateContext()) { - var items = context.All().Where(filter).ToList(); + var items = context.All() + .Where(s => !s.Protected && !s.DeletePending); + if (filter != null) + items = items.Where(filter); // check the removed skin is not the current user choice. if it is, switch back to default. Guid currentUserSkin = CurrentSkinInfo.Value.ID; if (items.Any(s => s.ID == currentUserSkin)) - scheduler.Add(() => CurrentSkinInfo.Value = SkinInfo.Default.ToLive()); + scheduler.Add(() => CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLive()); - skinModelManager.Delete(items, silent); + skinModelManager.Delete(items.ToList(), silent); } } From 68778674674e7f50598ba9e39d780417eff8a09f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 17:48:44 +0900 Subject: [PATCH 050/419] Make default fallback logic more robust --- .../Overlays/Settings/Sections/SkinSection.cs | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index bf0f6bf142..4de5d455fe 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -96,11 +96,9 @@ namespace osu.Game.Overlays.Settings.Sections updateItems(); - // Todo: This should not be necessary when OsuConfigManager is databased - if (!Guid.TryParse(configBindable.Value, out var configId) || skinDropdown.Items.All(s => s.ID != configId)) - configBindable.Value = string.Empty; + configBindable.BindValueChanged(id => Scheduler.AddOnce(updateSelectedSkinFromConfig)); + updateSelectedSkinFromConfig(); - configBindable.BindValueChanged(id => Scheduler.AddOnce(updateSelectedSkinFromConfig), true); dropdownBindable.BindValueChanged(skin => { if (skin.NewValue.Equals(random_skin_info)) @@ -124,20 +122,12 @@ namespace osu.Game.Overlays.Settings.Sections private void updateSelectedSkinFromConfig() { - if (!Guid.TryParse(configBindable.Value, out var configId)) return; + ILive skin = null; - var skin = skinDropdown.Items.FirstOrDefault(s => s.ID == configId); + if (Guid.TryParse(configBindable.Value, out var configId)) + skin = skinDropdown.Items.FirstOrDefault(s => s.ID == configId); - // TODO: i don't think this will be required any more. - if (skin == null) - { - // there may be a thread race condition where an item is selected that hasn't yet been added to the dropdown. - // to avoid adding complexity, let's just ensure the item is added so we can perform the selection. - skin = skins.Query(s => s.ID == configId); - updateItems(); - } - - dropdownBindable.Value = skin; + dropdownBindable.Value = skin ?? skinDropdown.Items.First(); } private void updateItems() From 0bcfb8e199004cb123598d9d5972d97ebf383cd2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 18:52:29 +0900 Subject: [PATCH 051/419] Fix regression in implementation of fix --- osu.Game/Database/RealmContextFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 6948918fe7..a20139e830 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -160,6 +160,7 @@ namespace osu.Game.Database if (!currentThreadCanCreateContexts.Value) { contextCreationLock.Wait(); + currentThreadCanCreateContexts.Value = true; tookSemaphoreLock = true; } else @@ -167,7 +168,6 @@ namespace osu.Game.Database // the semaphore is used to handle blocking of all context creation during certain periods. // once the semaphore has been taken by this code section, it is safe to create further contexts on the same thread. // this can happen if a realm subscription is active and triggers a callback which has user code that calls `CreateContext`. - currentThreadCanCreateContexts.Value = true; } contexts_created.Value++; From 566e10f8ccff29b3c0780832ec1811c13f1e4276 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Nov 2021 18:54:01 +0900 Subject: [PATCH 052/419] Refactor test to be easier to follow --- osu.Game.Tests/Database/GeneralUsageTests.cs | 36 +++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index fec1a0dedd..d15e34f2da 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -36,34 +36,36 @@ namespace osu.Game.Tests.Database }); } + /// + /// Test to ensure that a `CreateContext` call nested inside a subscription doesn't cause any deadlocks + /// due to context fetching semaphores. + /// [Test] - public void TestNestedContextCreation() + public void TestNestedContextCreationWithSubscription() { RunTestWithRealm((realmFactory, _) => { - var mainContext = realmFactory.Context; bool callbackRan = false; - var subscription = mainContext.All().SubscribeForNotifications((sender, changes, error) => + using (var context = realmFactory.CreateContext()) { - realmFactory.CreateContext(); - callbackRan = true; - }); - - Task.Factory.StartNew(() => - { - using (var threadContext = realmFactory.CreateContext()) + var subscription = context.All().SubscribeForNotifications((sender, changes, error) => { - threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); - } - }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + using (realmFactory.CreateContext()) + { + callbackRan = true; + } + }); - // will create a context but also run the callback above (Refresh is implicitly run when getting a new context). - realmFactory.CreateContext(); + // Force the callback above to run. + using (realmFactory.CreateContext()) + { + } + + subscription.Dispose(); + } Assert.IsTrue(callbackRan); - - subscription.Dispose(); }); } From 431ac1d97b951f988be2be3cbb87f300842f6457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Nov 2021 20:57:55 +0100 Subject: [PATCH 053/419] Remove unused using directive --- osu.Game.Tests/Database/GeneralUsageTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index d15e34f2da..841bf2de43 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -5,7 +5,6 @@ using System; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; -using osu.Game.Database; using osu.Game.Models; using Realms; From 0fc4d6dc2a85636d8390a144dcb13ee5ecab9eaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 28 Nov 2021 21:43:56 +0100 Subject: [PATCH 054/419] Implement beatmap card difficulty list --- .../TestSceneBeatmapCardDifficultyList.cs | 71 ++++++++++++ .../Cards/BeatmapCardDifficultyList.cs | 103 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs create mode 100644 osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs new file mode 100644 index 0000000000..aec75884d6 --- /dev/null +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs @@ -0,0 +1,71 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; + +namespace osu.Game.Tests.Visual.Beatmaps +{ + public class TestSceneBeatmapCardDifficultyList : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + var beatmapSet = new APIBeatmapSet + { + Beatmaps = new[] + { + new APIBeatmap { RulesetID = 1, StarRating = 5.76, DifficultyName = "Oni" }, + new APIBeatmap { RulesetID = 1, StarRating = 3.20, DifficultyName = "Muzukashii" }, + new APIBeatmap { RulesetID = 1, StarRating = 2.45, DifficultyName = "Futsuu" }, + + new APIBeatmap { RulesetID = 0, StarRating = 2.04, DifficultyName = "Normal" }, + new APIBeatmap { RulesetID = 0, StarRating = 3.51, DifficultyName = "Hard" }, + new APIBeatmap { RulesetID = 0, StarRating = 5.25, DifficultyName = "Insane" }, + + new APIBeatmap { RulesetID = 2, StarRating = 2.64, DifficultyName = "Salad" }, + new APIBeatmap { RulesetID = 2, StarRating = 3.56, DifficultyName = "Platter" }, + new APIBeatmap { RulesetID = 2, StarRating = 4.65, DifficultyName = "Rain" }, + + new APIBeatmap { RulesetID = 3, StarRating = 1.93, DifficultyName = "[7K] Normal" }, + new APIBeatmap { RulesetID = 3, StarRating = 3.18, DifficultyName = "[7K] Hyper" }, + new APIBeatmap { RulesetID = 3, StarRating = 4.82, DifficultyName = "[7K] Another" }, + + new APIBeatmap { RulesetID = 4, StarRating = 9.99, DifficultyName = "Unknown?!" }, + } + }; + + Child = new Container + { + Width = 300, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background2 + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(10), + Child = new BeatmapCardDifficultyList(beatmapSet) + } + } + }; + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs new file mode 100644 index 0000000000..7753d8480a --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs @@ -0,0 +1,103 @@ +// 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.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.Chat; +using osu.Game.Rulesets; +using osuTK; + +namespace osu.Game.Beatmaps.Drawables.Cards +{ + public class BeatmapCardDifficultyList : CompositeDrawable + { + public BeatmapCardDifficultyList(IBeatmapSetInfo beatmapSetInfo) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + FillFlowContainer flow; + + InternalChild = flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 3) + }; + + bool firstGroup = true; + + foreach (var group in beatmapSetInfo.Beatmaps.GroupBy(beatmap => beatmap.Ruleset.OnlineID).OrderBy(group => group.Key)) + { + if (!firstGroup) + { + flow.Add(Empty().With(s => + { + s.RelativeSizeAxes = Axes.X; + s.Height = 4; + })); + } + + foreach (var difficulty in group.OrderBy(b => b.StarRating)) + flow.Add(new BeatmapCardDifficultyRow(difficulty)); + + firstGroup = false; + } + } + + private class BeatmapCardDifficultyRow : CompositeDrawable + { + private readonly IBeatmapInfo beatmapInfo; + + public BeatmapCardDifficultyRow(IBeatmapInfo beatmapInfo) + { + this.beatmapInfo = beatmapInfo; + } + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4, 0), + Children = new[] + { + (rulesets.GetRuleset(beatmapInfo.Ruleset.OnlineID)?.CreateInstance().CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle }).With(icon => + { + icon.Anchor = icon.Origin = Anchor.CentreLeft; + icon.Size = new Vector2(16); + }), + new StarRatingDisplay(new StarDifficulty(beatmapInfo.StarRating, 0), StarRatingDisplaySize.Small) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + new LinkFlowContainer(s => + { + s.Font = OsuFont.Default.With(size: 14, weight: FontWeight.SemiBold); + }).With(d => + { + d.AutoSizeAxes = Axes.Both; + d.Anchor = Anchor.CentreLeft; + d.Origin = Anchor.CentreLeft; + d.Padding = new MarginPadding { Bottom = 2 }; + d.AddLink(beatmapInfo.DifficultyName, LinkAction.OpenBeatmap, beatmapInfo.OnlineID.ToString()); + }) + } + }; + } + } + } +} From 45656c359936b935e17b8ef0ec4b6ada043cc85f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Nov 2021 22:06:11 +0100 Subject: [PATCH 055/419] Fix difficulty spectrum display not ordering ruleset groups by ID --- osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs index f4501f0633..5b211084ab 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs @@ -62,7 +62,7 @@ namespace osu.Game.Beatmaps.Drawables // matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127 bool collapsed = beatmapSet.Beatmaps.Count() > 12; - foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset.OnlineID)) + foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset.OnlineID).OrderBy(group => group.Key)) { flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key, rulesetGrouping, collapsed)); } From 43546992586fcefc8f072deb091004edcf418175 Mon Sep 17 00:00:00 2001 From: MBmasher Date: Tue, 30 Nov 2021 12:51:23 +1100 Subject: [PATCH 056/419] Fix cumulative strain time calculation in Flashlight skill --- osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 466f0556ab..9c539d5e4b 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -45,11 +45,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills var osuPrevious = (OsuDifficultyHitObject)Previous[i]; var osuPreviousHitObject = (OsuHitObject)(osuPrevious.BaseObject); + OsuDifficultyHitObject osuLastPrevious; + if (i == 0) + osuLastPrevious = osuCurrent; + else + osuLastPrevious = (OsuDifficultyHitObject)Previous[i - 1]; + if (!(osuPrevious.BaseObject is Spinner)) { double jumpDistance = (osuHitObject.StackedPosition - osuPreviousHitObject.EndPosition).Length; - cumulativeStrainTime += osuPrevious.StrainTime; + cumulativeStrainTime += osuLastPrevious.StrainTime; // We want to nerf objects that can be easily seen within the Flashlight circle radius. if (i == 0) From 4a34a5c738fe5d0a765f85c1b167926bdc678952 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 30 Nov 2021 11:11:42 +0900 Subject: [PATCH 057/419] Refactor difficulty adjustment mod combinations test --- ...DifficultyAdjustmentModCombinationsTest.cs | 136 ++++++++++-------- 1 file changed, 75 insertions(+), 61 deletions(-) diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index e458e66ab7..612927337f 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -3,12 +3,14 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; +using osu.Game.Utils; namespace osu.Game.Tests.NonVisual { @@ -20,8 +22,10 @@ namespace osu.Game.Tests.NonVisual { var combinations = new TestLegacyDifficultyCalculator().CreateDifficultyAdjustmentModCombinations(); - Assert.AreEqual(1, combinations.Length); - Assert.IsTrue(combinations[0] is ModNoMod); + assertCombinations(new[] + { + new[] { typeof(ModNoMod) } + }, combinations); } [Test] @@ -29,9 +33,11 @@ namespace osu.Game.Tests.NonVisual { var combinations = new TestLegacyDifficultyCalculator(new ModA()).CreateDifficultyAdjustmentModCombinations(); - Assert.AreEqual(2, combinations.Length); - Assert.IsTrue(combinations[0] is ModNoMod); - Assert.IsTrue(combinations[1] is ModA); + assertCombinations(new[] + { + new[] { typeof(ModNoMod) }, + new[] { typeof(ModA) } + }, combinations); } [Test] @@ -39,14 +45,13 @@ namespace osu.Game.Tests.NonVisual { var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB()).CreateDifficultyAdjustmentModCombinations(); - Assert.AreEqual(4, combinations.Length); - Assert.IsTrue(combinations[0] is ModNoMod); - Assert.IsTrue(combinations[1] is ModA); - Assert.IsTrue(combinations[2] is MultiMod); - Assert.IsTrue(combinations[3] is ModB); - - Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA); - Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB); + assertCombinations(new[] + { + new[] { typeof(ModNoMod) }, + new[] { typeof(ModA) }, + new[] { typeof(ModA), typeof(ModB) }, + new[] { typeof(ModB) } + }, combinations); } [Test] @@ -54,10 +59,12 @@ namespace osu.Game.Tests.NonVisual { var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModIncompatibleWithA()).CreateDifficultyAdjustmentModCombinations(); - Assert.AreEqual(3, combinations.Length); - Assert.IsTrue(combinations[0] is ModNoMod); - Assert.IsTrue(combinations[1] is ModA); - Assert.IsTrue(combinations[2] is ModIncompatibleWithA); + assertCombinations(new[] + { + new[] { typeof(ModNoMod) }, + new[] { typeof(ModA) }, + new[] { typeof(ModIncompatibleWithA) } + }, combinations); } [Test] @@ -65,22 +72,17 @@ namespace osu.Game.Tests.NonVisual { var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB(), new ModIncompatibleWithA(), new ModIncompatibleWithAAndB()).CreateDifficultyAdjustmentModCombinations(); - Assert.AreEqual(8, combinations.Length); - Assert.IsTrue(combinations[0] is ModNoMod); - Assert.IsTrue(combinations[1] is ModA); - Assert.IsTrue(combinations[2] is MultiMod); - Assert.IsTrue(combinations[3] is ModB); - Assert.IsTrue(combinations[4] is MultiMod); - Assert.IsTrue(combinations[5] is ModIncompatibleWithA); - Assert.IsTrue(combinations[6] is MultiMod); - Assert.IsTrue(combinations[7] is ModIncompatibleWithAAndB); - - Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA); - Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB); - Assert.IsTrue(((MultiMod)combinations[4]).Mods[0] is ModB); - Assert.IsTrue(((MultiMod)combinations[4]).Mods[1] is ModIncompatibleWithA); - Assert.IsTrue(((MultiMod)combinations[6]).Mods[0] is ModIncompatibleWithA); - Assert.IsTrue(((MultiMod)combinations[6]).Mods[1] is ModIncompatibleWithAAndB); + assertCombinations(new[] + { + new[] { typeof(ModNoMod) }, + new[] { typeof(ModA) }, + new[] { typeof(ModA), typeof(ModB) }, + new[] { typeof(ModB) }, + new[] { typeof(ModB), typeof(ModIncompatibleWithA) }, + new[] { typeof(ModIncompatibleWithA) }, + new[] { typeof(ModIncompatibleWithA), typeof(ModIncompatibleWithAAndB) }, + new[] { typeof(ModIncompatibleWithAAndB) }, + }, combinations); } [Test] @@ -88,10 +90,12 @@ namespace osu.Game.Tests.NonVisual { var combinations = new TestLegacyDifficultyCalculator(new ModAofA(), new ModIncompatibleWithAofA()).CreateDifficultyAdjustmentModCombinations(); - Assert.AreEqual(3, combinations.Length); - Assert.IsTrue(combinations[0] is ModNoMod); - Assert.IsTrue(combinations[1] is ModAofA); - Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA); + assertCombinations(new[] + { + new[] { typeof(ModNoMod) }, + new[] { typeof(ModAofA) }, + new[] { typeof(ModIncompatibleWithAofA) } + }, combinations); } [Test] @@ -99,17 +103,13 @@ namespace osu.Game.Tests.NonVisual { var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModC())).CreateDifficultyAdjustmentModCombinations(); - Assert.AreEqual(4, combinations.Length); - Assert.IsTrue(combinations[0] is ModNoMod); - Assert.IsTrue(combinations[1] is ModA); - Assert.IsTrue(combinations[2] is MultiMod); - Assert.IsTrue(combinations[3] is MultiMod); - - Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA); - Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB); - Assert.IsTrue(((MultiMod)combinations[2]).Mods[2] is ModC); - Assert.IsTrue(((MultiMod)combinations[3]).Mods[0] is ModB); - Assert.IsTrue(((MultiMod)combinations[3]).Mods[1] is ModC); + assertCombinations(new[] + { + new[] { typeof(ModNoMod) }, + new[] { typeof(ModA) }, + new[] { typeof(ModA), typeof(ModB), typeof(ModC) }, + new[] { typeof(ModB), typeof(ModC) } + }, combinations); } [Test] @@ -117,13 +117,12 @@ namespace osu.Game.Tests.NonVisual { var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModIncompatibleWithA())).CreateDifficultyAdjustmentModCombinations(); - Assert.AreEqual(3, combinations.Length); - Assert.IsTrue(combinations[0] is ModNoMod); - Assert.IsTrue(combinations[1] is ModA); - Assert.IsTrue(combinations[2] is MultiMod); - - Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModB); - Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModIncompatibleWithA); + assertCombinations(new[] + { + new[] { typeof(ModNoMod) }, + new[] { typeof(ModA) }, + new[] { typeof(ModB), typeof(ModIncompatibleWithA) } + }, combinations); } [Test] @@ -131,13 +130,28 @@ namespace osu.Game.Tests.NonVisual { var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModA(), new ModB())).CreateDifficultyAdjustmentModCombinations(); - Assert.AreEqual(3, combinations.Length); - Assert.IsTrue(combinations[0] is ModNoMod); - Assert.IsTrue(combinations[1] is ModA); - Assert.IsTrue(combinations[2] is MultiMod); + assertCombinations(new[] + { + new[] { typeof(ModNoMod) }, + new[] { typeof(ModA) }, + new[] { typeof(ModA), typeof(ModB) } + }, combinations); + } - Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA); - Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB); + private void assertCombinations(Type[][] expectedCombinations, Mod[] actualCombinations) + { + Assert.AreEqual(expectedCombinations.Length, actualCombinations.Length); + + foreach (Type[] expected in expectedCombinations) + { + Type[] actualTypes = ModUtils.FlattenMods(actualCombinations).Select(m => m.GetType()).ToArray(); + + Assert.Multiple(() => + { + foreach (var expectedType in expected) + Assert.Contains(expectedType, actualTypes); + }); + } } private class ModA : Mod From 35d68d6ab039318bf024853ca3214c4d25368d2d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 11:47:29 +0900 Subject: [PATCH 058/419] Remove all optimisations from `RealmLive` --- osu.Game/Database/RealmLive.cs | 43 +++++++++------------------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 7ae7d8544a..0b422fd36f 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Threading; +using osu.Framework.Development; using Realms; #nullable enable @@ -17,10 +17,7 @@ namespace osu.Game.Database { public Guid ID { get; } - public bool IsManaged { get; } - - private readonly SynchronizationContext? fetchedContext; - private readonly int fetchedThreadId; + public bool IsManaged => data.IsManaged; /// /// The original live data used to create this instance. @@ -35,14 +32,6 @@ namespace osu.Game.Database { this.data = data; - if (data.IsManaged) - { - IsManaged = true; - - fetchedContext = SynchronizationContext.Current; - fetchedThreadId = Thread.CurrentThread.ManagedThreadId; - } - ID = data.ID; } @@ -52,7 +41,7 @@ namespace osu.Game.Database /// The action to perform. public void PerformRead(Action perform) { - if (originalDataValid) + if (!IsManaged) { perform(data); return; @@ -71,7 +60,7 @@ namespace osu.Game.Database if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn))) throw new InvalidOperationException($"Realm live objects should not exit the scope of {nameof(PerformRead)}."); - if (originalDataValid) + if (!IsManaged) return perform(data); using (var realm = Realm.GetInstance(data.Realm.Config)) @@ -99,31 +88,21 @@ namespace osu.Game.Database { get { - if (originalDataValid) + if (!IsManaged) return data; - if (!isCorrectThread) - throw new InvalidOperationException($"Can't use {nameof(Value)} unless on the same thread the original data was fetched from."); + if (!ThreadSafety.IsUpdateThread) + throw new InvalidOperationException($"Can't use {nameof(Value)} on managed objects from non-update threads"); + + // When using Value, we rely on garbage collection for the realm instance used to retrieve the instance. + // As we are sure that this is on the same thread var realm = Realm.GetInstance(data.Realm.Config); - var retrieved = realm.Find(ID); - if (!retrieved.IsValid) - throw new InvalidOperationException("Attempted to access value without an open context"); - - return retrieved; + return realm.Find(ID); } } - // TODO: Revisit adding these conditionals back as an optimisation: || (isCorrectThread && data.IsValid); - // They have temporarily been removed due to an oversight involving .AsQueryable, see https://github.com/realm/realm-dotnet/discussions/2734. - // This means we are fetching a new context every `PerformRead` or `PerformWrite`, even when on the correct thread. - private bool originalDataValid => !IsManaged; - - // this matches realm's internal thread validation (see https://github.com/realm/realm-dotnet/blob/903b4d0b304f887e37e2d905384fb572a6496e70/Realm/Realm/Native/SynchronizationContextScheduler.cs#L72) - private bool isCorrectThread - => (fetchedContext != null && SynchronizationContext.Current == fetchedContext) || fetchedThreadId == Thread.CurrentThread.ManagedThreadId; - public bool Equals(ILive? other) => ID == other?.ID; } } From 2e31f5a338988fb69dd77c709c1e36f742dbb943 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 11:55:13 +0900 Subject: [PATCH 059/419] Update tests to match new behaviour --- osu.Game.Tests/Database/RealmLiveTests.cs | 107 +++++++++++----------- 1 file changed, 53 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 63566394d6..41c82399dc 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -46,53 +46,6 @@ namespace osu.Game.Tests.Database Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden)); } - [Test] - public void TestValueAccessWithOpenContext() - { - RunTestWithRealm((realmFactory, _) => - { - RealmLive? liveBeatmap = null; - Task.Factory.StartNew(() => - { - using (var threadContext = realmFactory.CreateContext()) - { - var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); - - liveBeatmap = beatmap.ToLive(); - } - }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); - - Debug.Assert(liveBeatmap != null); - - Task.Factory.StartNew(() => - { - // TODO: The commented code is the behaviour we hope to obtain, but is temporarily disabled. - // See https://github.com/ppy/osu/pull/15851 - using (realmFactory.CreateContext()) - { - Assert.Throws(() => - { - var __ = liveBeatmap.Value; - }); - } - - // Assert.DoesNotThrow(() => - // { - // using (realmFactory.CreateContext()) - // { - // var resolved = liveBeatmap.Value; - // - // Assert.IsTrue(resolved.Realm.IsClosed); - // Assert.IsTrue(resolved.IsValid); - // - // // can access properties without a crash. - // Assert.IsFalse(resolved.Hidden); - // } - // }); - }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); - }); - } - [Test] public void TestScopedReadWithoutContext() { @@ -148,6 +101,59 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestValueAccessNonManaged() + { + RunTestWithRealm((realmFactory, _) => + { + var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); + var liveBeatmap = beatmap.ToLive(); + + Assert.DoesNotThrow(() => + { + var __ = liveBeatmap.Value; + }); + }); + } + + [Test] + public void TestValueAccessWithOpenContextFails() + { + RunTestWithRealm((realmFactory, _) => + { + RealmLive? liveBeatmap = null; + Task.Factory.StartNew(() => + { + using (var threadContext = realmFactory.CreateContext()) + { + var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + + liveBeatmap = beatmap.ToLive(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + + Debug.Assert(liveBeatmap != null); + + Task.Factory.StartNew(() => + { + // Can't be used, without a valid context. + Assert.Throws(() => + { + var __ = liveBeatmap.Value; + }); + + // Can't be used, even from within a valid context. + using (realmFactory.CreateContext()) + { + Assert.Throws(() => + { + var __ = liveBeatmap.Value; + }); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + }); + } + [Test] public void TestValueAccessWithoutOpenContextFails() { @@ -209,8 +215,6 @@ namespace osu.Game.Tests.Database Assert.AreEqual(0, updateThreadContext.All().Count()); Assert.AreEqual(0, changesTriggered); - // TODO: Originally the following was using `liveBeatmap.Value`. This has been temporarily disabled. - // See https://github.com/ppy/osu/pull/15851 liveBeatmap.PerformRead(resolved => { // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point. @@ -218,11 +222,6 @@ namespace osu.Game.Tests.Database Assert.AreEqual(2, updateThreadContext.All().Count()); Assert.AreEqual(1, changesTriggered); - // TODO: as above, temporarily disabled as it doesn't make sense inside a `PerformRead`. - // // even though the realm that this instance was resolved for was closed, it's still valid. - // Assert.IsTrue(resolved.Realm.IsClosed); - // Assert.IsTrue(resolved.IsValid); - // can access properties without a crash. Assert.IsFalse(resolved.Hidden); From f3f77fa05316faa2e046edf7ef116058f190c9fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 11:56:53 +0900 Subject: [PATCH 060/419] Update missed xmldoc/comments --- osu.Game/Database/ILive.cs | 4 ++-- osu.Game/Database/RealmLive.cs | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/ILive.cs b/osu.Game/Database/ILive.cs index a863339f11..3011754bc1 100644 --- a/osu.Game/Database/ILive.cs +++ b/osu.Game/Database/ILive.cs @@ -38,10 +38,10 @@ namespace osu.Game.Database bool IsManaged { get; } /// - /// Resolve the value of this instance on the current thread's context. + /// Resolve the value of this instance on the update thread. /// /// - /// After resolving the data should not be passed between threads. + /// After resolving, the data should not be passed between threads. /// T Value { get; } } diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 0b422fd36f..2accea305a 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -95,8 +95,7 @@ namespace osu.Game.Database throw new InvalidOperationException($"Can't use {nameof(Value)} on managed objects from non-update threads"); // When using Value, we rely on garbage collection for the realm instance used to retrieve the instance. - // As we are sure that this is on the same thread - + // As we are sure that this is on the update thread, there should always be an open and constantly refreshing realm instance to ensure file size growth is a non-issue. var realm = Realm.GetInstance(data.Realm.Config); return realm.Find(ID); From a73919917c3f829bee58a827c9071a8e145ddb51 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 12:32:08 +0900 Subject: [PATCH 061/419] Fix intermittent test failures in `TestScenBeatmapInfoWedge` due to async load https://github.com/ppy/osu/runs/4358685294?check_suite_focus=true Occurs due to the wedge content also reloading on ruleset change, which wasn't being accounted for. In a fail case, the content would change during the "select beatmap" step's async load wait, causing incorrect results. https://github.com/ppy/osu/blob/51a353e12db189f9958228d30fe045b8460c6b92/osu.Game/Screens/Select/BeatmapInfoWedge.cs#L70 --- .../SongSelect/TestSceneBeatmapInfoWedge.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index e7c54efa8c..9ad5242df4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.SongSelect beatmaps.Add(testBeatmap); - AddStep("set ruleset", () => Ruleset.Value = rulesetInfo); + setRuleset(rulesetInfo); selectBeatmap(testBeatmap); @@ -167,6 +167,22 @@ namespace osu.Game.Tests.Visual.SongSelect label => label.Statistic.Name == "BPM" && label.Statistic.Content == target.ToString(CultureInfo.InvariantCulture))); } + private void setRuleset(RulesetInfo rulesetInfo) + { + Container containerBefore = null; + + AddStep("set ruleset", () => + { + // wedge content is only refreshed if the ruleset changes, so only wait for load in that case. + if (!rulesetInfo.Equals(Ruleset.Value)) + containerBefore = infoWedge.DisplayedContent; + + Ruleset.Value = rulesetInfo; + }); + + AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); + } + private void selectBeatmap([CanBeNull] IBeatmap b) { Container containerBefore = null; From f921acc681fbef678c39a15cc3502b58d286148c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 13:01:40 +0900 Subject: [PATCH 062/419] Fix chat tab dropdown not being reachable at default sizing Would have liked to fix this in a more local way, but the structure of the dropdowns is just a pain in the ass to work with, so this will do for now. --- osu.Game/Graphics/UserInterface/OsuTabDropdown.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs b/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs index 68ffc6bf4e..b7e25ae4e7 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs @@ -66,7 +66,7 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.TopRight; BackgroundColour = Color4.Black.Opacity(0.7f); - MaxHeight = 400; + MaxHeight = 200; } protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableOsuTabDropdownMenuItem(item); From 384a0664c27101c9f32712942e817913a12cb73e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 14:05:44 +0900 Subject: [PATCH 063/419] Remove unused migration method --- osu.Game/OsuGameBase.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 88c9ab370c..bfc0da468b 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -442,10 +442,6 @@ namespace osu.Game protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage); - private void migrateDataToRealm() - { - } - private void onRulesetChanged(ValueChangedEvent r) { if (r.NewValue?.Available != true) From 6e4cd91b7bb782572e2e7abbd5b6dc9926dca95e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 14:12:36 +0900 Subject: [PATCH 064/419] Fix update thread realm context never being `Refresh()`ed --- osu.Game/OsuGameBase.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index bfc0da468b..6eb67b34e8 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -377,6 +377,13 @@ namespace osu.Game FrameStatistics.ValueChanged += e => fpsDisplayVisible.Value = e.NewValue != FrameStatisticsMode.None; } + protected override void Update() + { + base.Update(); + + realmFactory.Refresh(); + } + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); From 0feec0996665a0009479d4667a4500d3edd8b8b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 14:12:49 +0900 Subject: [PATCH 065/419] Refactor beatmap importer tests to ensure realm is refreshed when waiting on state --- .../Database/BeatmapImporterTests.cs | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index d2193350ad..a6edd6cb5f 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -852,7 +852,11 @@ namespace osu.Game.Tests.Database { IQueryable? resultSets = null; - waitForOrAssert(() => (resultSets = realm.All().Where(s => !s.DeletePending && s.OnlineID == 241526)).Any(), + waitForOrAssert(() => + { + realm.Refresh(); + return (resultSets = realm.All().Where(s => !s.DeletePending && s.OnlineID == 241526)).Any(); + }, @"BeatmapSet did not import to the database in allocated time.", timeout); // ensure we were stored to beatmap database backing... @@ -865,16 +869,16 @@ namespace osu.Game.Tests.Database // ReSharper disable once PossibleUnintendedReferenceComparison IEnumerable queryBeatmaps() => realm.All().Where(s => s.BeatmapSet != null && s.BeatmapSet == set); - waitForOrAssert(() => queryBeatmaps().Count() == 12, @"Beatmaps did not import to the database in allocated time", timeout); - waitForOrAssert(() => queryBeatmapSets().Count() == 1, @"BeatmapSet did not import to the database in allocated time", timeout); + Assert.AreEqual(12, queryBeatmaps().Count(), @"Beatmap count was not correct"); + Assert.AreEqual(1, queryBeatmapSets().Count(), @"Beatmapset count was not correct"); - int countBeatmapSetBeatmaps = 0; - int countBeatmaps = 0; + int countBeatmapSetBeatmaps; + int countBeatmaps; - waitForOrAssert(() => - (countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) == - (countBeatmaps = queryBeatmaps().Count()), - $@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).", timeout); + Assert.AreEqual( + countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count, + countBeatmaps = queryBeatmaps().Count(), + $@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps})."); foreach (RealmBeatmap b in set.Beatmaps) Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineID == b.OnlineID)); From 6bf9327228e66b49e4c3cd590e055eb5bf7bad8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 15:17:16 +0900 Subject: [PATCH 066/419] Add linking property on `SkinFileInfo` to allow EF to understand the link post-rename --- osu.Game/Database/OsuDbContext.cs | 1 + osu.Game/Skinning/SkinFileInfo.cs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index 26f287da26..13d362e0be 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -135,6 +135,7 @@ namespace osu.Game.Database modelBuilder.Entity().HasIndex(b => b.Hash).IsUnique(); modelBuilder.Entity().HasIndex(b => b.DeletePending); + modelBuilder.Entity().HasMany(s => s.Files).WithOne(f => f.SkinInfo); modelBuilder.Entity().HasIndex(b => new { b.RulesetID, b.Variant }); diff --git a/osu.Game/Skinning/SkinFileInfo.cs b/osu.Game/Skinning/SkinFileInfo.cs index db7cd953bb..4a86a6ed94 100644 --- a/osu.Game/Skinning/SkinFileInfo.cs +++ b/osu.Game/Skinning/SkinFileInfo.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using osu.Game.Database; using osu.Game.IO; @@ -13,6 +14,8 @@ namespace osu.Game.Skinning public int SkinInfoID { get; set; } + public EFSkinInfo SkinInfo { get; set; } + public int FileInfoID { get; set; } public FileInfo FileInfo { get; set; } From a943089be261495bbc7c1aec19bff731d5277543 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 15:17:26 +0900 Subject: [PATCH 067/419] Add support for migration of skins from EF to Realm --- osu.Game/Database/RealmContextFactory.cs | 56 ++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 911c88a61f..290b42311c 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using System.Reflection; using System.Threading; +using Microsoft.EntityFrameworkCore; using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Input.Bindings; @@ -205,6 +206,61 @@ namespace osu.Game.Database return; using (var db = efContextFactory.GetForWrite()) + { + migrateSettings(db); + migrateSkins(db); + } + + void migrateSkins(DatabaseWriteUsage db) + { + // migrate ruleset settings. can be removed 20220530. + var existingSkins = db.Context.SkinInfo + .Include(s => s.Files) + .ThenInclude(f => f.FileInfo); + + // previous entries in EF are removed post migration. + if (!existingSkins.Any()) + return; + + using (var realm = CreateContext()) + using (var transaction = realm.BeginWrite()) + { + // only migrate data if the realm database is empty. + if (!realm.All().Any(s => !s.Protected)) + { + foreach (var skin in existingSkins) + { + var realmSkin = new SkinInfo + { + Name = skin.Name, + Creator = skin.Creator, + Hash = skin.Hash, + Protected = false, + InstantiationInfo = skin.InstantiationInfo, + }; + + foreach (var file in skin.Files) + { + var realmFile = realm.Find(file.FileInfo.Hash); + + if (realmFile == null) + realm.Add(realmFile = new RealmFile { Hash = file.FileInfo.Hash }); + + realmSkin.Files.Add(new RealmNamedFileUsage(realmFile, file.Filename)); + } + + realm.Add(realmSkin); + } + } + + db.Context.RemoveRange(existingSkins); + // Intentionally don't clean up the files, so they don't get purged by EF. + + transaction.Commit(); + } + } + + void migrateSettings(DatabaseWriteUsage db) { // migrate ruleset settings. can be removed 20220315. var existingSettings = db.Context.DatabasedSetting; From 0efd565c8b974986effde26819648030eb8ea326 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 15:41:07 +0900 Subject: [PATCH 068/419] Remove forgotten using statement --- osu.Game/Skinning/SkinFileInfo.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Skinning/SkinFileInfo.cs b/osu.Game/Skinning/SkinFileInfo.cs index 4a86a6ed94..f414687b33 100644 --- a/osu.Game/Skinning/SkinFileInfo.cs +++ b/osu.Game/Skinning/SkinFileInfo.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; using osu.Game.Database; using osu.Game.IO; From 6f66ecd77be4d6e5fc9b9a03308b2600ebf92c31 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 15:41:18 +0900 Subject: [PATCH 069/419] Move migrations to own file and add user skin choice config migration --- osu.Game/Database/EFToRealmMigrator.cs | 147 +++++++++++++++++++++++ osu.Game/Database/RealmContextFactory.cs | 107 ----------------- osu.Game/OsuGameBase.cs | 2 + 3 files changed, 149 insertions(+), 107 deletions(-) create mode 100644 osu.Game/Database/EFToRealmMigrator.cs diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs new file mode 100644 index 0000000000..670eb95ad6 --- /dev/null +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -0,0 +1,147 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using Microsoft.EntityFrameworkCore; +using osu.Game.Configuration; +using osu.Game.Models; +using osu.Game.Skinning; + +#nullable enable + +namespace osu.Game.Database +{ + internal class EFToRealmMigrator + { + private readonly DatabaseContextFactory efContextFactory; + private readonly RealmContextFactory realmContextFactory; + private readonly OsuConfigManager config; + + public EFToRealmMigrator(DatabaseContextFactory efContextFactory, RealmContextFactory realmContextFactory, OsuConfigManager config) + { + this.efContextFactory = efContextFactory; + this.realmContextFactory = realmContextFactory; + this.config = config; + } + + public void Run() + { + using (var db = efContextFactory.GetForWrite()) + { + migrateSettings(db); + migrateSkins(db); + } + } + + private void migrateSkins(DatabaseWriteUsage db) + { + var userSkinChoice = config.GetBindable(OsuSetting.Skin); + int.TryParse(userSkinChoice.Value, out int userSkinInt); + + switch (userSkinInt) + { + case EFSkinInfo.DEFAULT_SKIN: + userSkinChoice.Value = SkinInfo.DEFAULT_SKIN.ToString(); + break; + + case EFSkinInfo.CLASSIC_SKIN: + userSkinChoice.Value = SkinInfo.CLASSIC_SKIN.ToString(); + break; + } + + // migrate ruleset settings. can be removed 20220530. + var existingSkins = db.Context.SkinInfo + .Include(s => s.Files) + .ThenInclude(f => f.FileInfo) + .ToList(); + + // previous entries in EF are removed post migration. + if (!existingSkins.Any()) + return; + + using (var realm = realmContextFactory.CreateContext()) + using (var transaction = realm.BeginWrite()) + { + // only migrate data if the realm database is empty. + if (!realm.All().Any(s => !s.Protected)) + { + foreach (var skin in existingSkins) + { + var realmSkin = new SkinInfo + { + Name = skin.Name, + Creator = skin.Creator, + Hash = skin.Hash, + Protected = false, + InstantiationInfo = skin.InstantiationInfo, + }; + + foreach (var file in skin.Files) + { + var realmFile = realm.Find(file.FileInfo.Hash); + + if (realmFile == null) + realm.Add(realmFile = new RealmFile { Hash = file.FileInfo.Hash }); + + realmSkin.Files.Add(new RealmNamedFileUsage(realmFile, file.Filename)); + } + + realm.Add(realmSkin); + + if (skin.ID == userSkinInt) + userSkinChoice.Value = realmSkin.ID.ToString(); + } + } + + db.Context.RemoveRange(existingSkins); + // Intentionally don't clean up the files, so they don't get purged by EF. + + transaction.Commit(); + } + } + + private void migrateSettings(DatabaseWriteUsage db) + { + // migrate ruleset settings. can be removed 20220315. + var existingSettings = db.Context.DatabasedSetting; + + // previous entries in EF are removed post migration. + if (!existingSettings.Any()) + return; + + using (var realm = realmContextFactory.CreateContext()) + using (var transaction = realm.BeginWrite()) + { + // only migrate data if the realm database is empty. + if (!realm.All().Any()) + { + foreach (var dkb in existingSettings) + { + if (dkb.RulesetID == null) + continue; + + string? shortName = getRulesetShortNameFromLegacyID(dkb.RulesetID.Value); + + if (string.IsNullOrEmpty(shortName)) + continue; + + realm.Add(new RealmRulesetSetting + { + Key = dkb.Key, + Value = dkb.StringValue, + RulesetName = shortName, + Variant = dkb.Variant ?? 0, + }); + } + } + + db.Context.RemoveRange(existingSettings); + + transaction.Commit(); + } + } + + private string? getRulesetShortNameFromLegacyID(long rulesetId) => + efContextFactory.Get().RulesetInfo.FirstOrDefault(r => r.ID == rulesetId)?.ShortName; + } +} diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 290b42311c..c6acd3097c 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -5,7 +5,6 @@ using System; using System.Linq; using System.Reflection; using System.Threading; -using Microsoft.EntityFrameworkCore; using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Input.Bindings; @@ -103,10 +102,6 @@ namespace osu.Game.Database // This method triggers the first `CreateContext` call, which will implicitly run realm migrations and bring the schema up-to-date. cleanupPendingDeletions(); - - // Data migration is handled separately from schema migrations. - // This is required as the user may be initialising realm for the first time ever, which would result in no schema migrations running. - migrateDataFromEF(); } private void cleanupPendingDeletions() @@ -200,108 +195,6 @@ namespace osu.Game.Database }; } - private void migrateDataFromEF() - { - if (efContextFactory == null) - return; - - using (var db = efContextFactory.GetForWrite()) - { - migrateSettings(db); - migrateSkins(db); - } - - void migrateSkins(DatabaseWriteUsage db) - { - // migrate ruleset settings. can be removed 20220530. - var existingSkins = db.Context.SkinInfo - .Include(s => s.Files) - .ThenInclude(f => f.FileInfo); - - // previous entries in EF are removed post migration. - if (!existingSkins.Any()) - return; - - using (var realm = CreateContext()) - using (var transaction = realm.BeginWrite()) - { - // only migrate data if the realm database is empty. - if (!realm.All().Any(s => !s.Protected)) - { - foreach (var skin in existingSkins) - { - var realmSkin = new SkinInfo - { - Name = skin.Name, - Creator = skin.Creator, - Hash = skin.Hash, - Protected = false, - InstantiationInfo = skin.InstantiationInfo, - }; - - foreach (var file in skin.Files) - { - var realmFile = realm.Find(file.FileInfo.Hash); - - if (realmFile == null) - realm.Add(realmFile = new RealmFile { Hash = file.FileInfo.Hash }); - - realmSkin.Files.Add(new RealmNamedFileUsage(realmFile, file.Filename)); - } - - realm.Add(realmSkin); - } - } - - db.Context.RemoveRange(existingSkins); - // Intentionally don't clean up the files, so they don't get purged by EF. - - transaction.Commit(); - } - } - - void migrateSettings(DatabaseWriteUsage db) - { - // migrate ruleset settings. can be removed 20220315. - var existingSettings = db.Context.DatabasedSetting; - - // previous entries in EF are removed post migration. - if (!existingSettings.Any()) - return; - - using (var realm = CreateContext()) - using (var transaction = realm.BeginWrite()) - { - // only migrate data if the realm database is empty. - if (!realm.All().Any()) - { - foreach (var dkb in existingSettings) - { - if (dkb.RulesetID == null) - continue; - - string? shortName = getRulesetShortNameFromLegacyID(dkb.RulesetID.Value); - - if (string.IsNullOrEmpty(shortName)) - continue; - - realm.Add(new RealmRulesetSetting - { - Key = dkb.Key, - Value = dkb.StringValue, - RulesetName = shortName, - Variant = dkb.Variant ?? 0, - }); - } - } - - db.Context.RemoveRange(existingSettings); - - transaction.Commit(); - } - } - } - private void onMigration(Migration migration, ulong lastSchemaVersion) { for (ulong i = lastSchemaVersion + 1; i <= schema_version; i++) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 4bae6f3c1d..f6e2c780ed 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -198,6 +198,8 @@ namespace osu.Game dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", contextFactory)); + new EFToRealmMigrator(contextFactory, realmFactory, LocalConfig).Run(); + dependencies.CacheAs(Storage); var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures"))); From 049d9ce5ef5b906278b39bf83d9e5eac17284993 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 17:09:32 +0900 Subject: [PATCH 070/419] Add test coverage of match type propagating to other users' settings --- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index c70906927e..7763e11609 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -11,6 +11,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Rooms; using osu.Game.Rulesets; @@ -118,6 +119,25 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("user still on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); } + [Test] + public void TestSettingsUpdatedWhenChangingQueueMode() + { + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + Type = { Value = MatchType.TeamVersus } + }); + + AddUntilStep("match type versus", () => client.APIRoom?.Type.Value == MatchType.HeadToHead); + + AddStep("change match type", () => client.ChangeSettings(new MultiplayerRoomSettings + { + MatchType = MatchType.TeamVersus + })); + + AddUntilStep("api room updated", () => client.APIRoom?.Type.Value == MatchType.TeamVersus); + } + [Test] public void TestChangeTypeViaMatchSettings() { From 25b9575de01d4a7d35dc19ece9b6748506f60cab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 17:09:40 +0900 Subject: [PATCH 071/419] Fix missing transfer of match type to settings --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 4c472164d6..02fb94133a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -694,6 +694,7 @@ namespace osu.Game.Online.Multiplayer Room.Settings = settings; APIRoom.Name.Value = Room.Settings.Name; APIRoom.Password.Value = Room.Settings.Password; + APIRoom.Type.Value = Room.Settings.MatchType; APIRoom.QueueMode.Value = Room.Settings.QueueMode; RoomUpdated?.Invoke(); From 8de06803a8f3285d683c8db38a818e5a9c4b4ced Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 17:16:30 +0900 Subject: [PATCH 072/419] Fix incorrect test step text Co-authored-by: Dan Balasescu --- osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 7763e11609..e00939a4de 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] - public void TestSettingsUpdatedWhenChangingQueueMode() + public void TestSettingsUpdatedWhenChangingMatchType() { createRoom(() => new Room { From 23e297d414815477ed71f4e6e9360aee2bb8f7ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 19:08:51 +0900 Subject: [PATCH 073/419] Log output response sizes Visibility is the first step towards action. Or something. --- osu.Game/Online/API/APIRequest.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 43195811dc..efb0b102d0 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -38,7 +38,12 @@ namespace osu.Game.Online.API protected override void PostProcess() { base.PostProcess(); - Response = ((OsuJsonWebRequest)WebRequest)?.ResponseObject; + + if (WebRequest != null) + { + Response = ((OsuJsonWebRequest)WebRequest).ResponseObject; + Logger.Log($"{GetType()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes"); + } } internal void TriggerSuccess(T result) From 87883f1fe433608090aebdeaa981632310fa5158 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 19:27:43 +0900 Subject: [PATCH 074/419] Add `BeatmapLookupCache` --- osu.Game/Database/BeatmapLookupCache.cs | 143 ++++++++++++++++++++++++ osu.Game/OsuGameBase.cs | 4 + 2 files changed, 147 insertions(+) create mode 100644 osu.Game/Database/BeatmapLookupCache.cs diff --git a/osu.Game/Database/BeatmapLookupCache.cs b/osu.Game/Database/BeatmapLookupCache.cs new file mode 100644 index 0000000000..c4e20d59b6 --- /dev/null +++ b/osu.Game/Database/BeatmapLookupCache.cs @@ -0,0 +1,143 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Database +{ + // This class is based on `UserLookupCache` which is well tested. + // If modifications are to be made here, a base abstract implementation should likely be created and shared between the two. + public class BeatmapLookupCache : MemoryCachingComponent + { + [Resolved] + private IAPIProvider api { get; set; } + + /// + /// Perform an API lookup on the specified beatmap, populating a model. + /// + /// The beatmap to lookup. + /// An optional cancellation token. + /// The populated beatmap, or null if the beatmap does not exist or the request could not be satisfied. + [ItemCanBeNull] + public Task GetBeatmapAsync(int beatmapId, CancellationToken token = default) => GetAsync(beatmapId, token); + + /// + /// Perform an API lookup on the specified beatmaps, populating a model. + /// + /// The beatmaps to lookup. + /// An optional cancellation token. + /// The populated beatmaps. May include null results for failed retrievals. + public Task GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) + { + var beatmapLookupTasks = new List>(); + + foreach (int u in beatmapIds) + { + beatmapLookupTasks.Add(GetBeatmapAsync(u, token).ContinueWith(task => + { + if (!task.IsCompletedSuccessfully) + return null; + + return task.Result; + }, token)); + } + + return Task.WhenAll(beatmapLookupTasks); + } + + protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default) + => await queryBeatmap(lookup).ConfigureAwait(false); + + private readonly Queue<(int id, TaskCompletionSource)> pendingBeatmapTasks = new Queue<(int, TaskCompletionSource)>(); + private Task pendingRequestTask; + private readonly object taskAssignmentLock = new object(); + + private Task queryBeatmap(int beatmapId) + { + lock (taskAssignmentLock) + { + var tcs = new TaskCompletionSource(); + + // Add to the queue. + pendingBeatmapTasks.Enqueue((beatmapId, tcs)); + + // Create a request task if there's not already one. + if (pendingRequestTask == null) + createNewTask(); + + return tcs.Task; + } + } + + private void performLookup() + { + // contains at most 50 unique beatmap IDs from beatmapTasks, which is used to perform the lookup. + var beatmapTasks = new Dictionary>>(); + + // Grab at most 50 unique beatmap IDs from the queue. + lock (taskAssignmentLock) + { + while (pendingBeatmapTasks.Count > 0 && beatmapTasks.Count < 1) + { + (int id, TaskCompletionSource task) next = pendingBeatmapTasks.Dequeue(); + + // Perform a secondary check for existence, in case the beatmap was queried in a previous batch. + if (CheckExists(next.id, out var existing)) + next.task.SetResult(existing); + else + { + if (beatmapTasks.TryGetValue(next.id, out var tasks)) + tasks.Add(next.task); + else + beatmapTasks[next.id] = new List> { next.task }; + } + } + } + + // Query the beatmaps. + var request = new GetBeatmapRequest(new APIBeatmap { OnlineID = beatmapTasks.Keys.First() }); + + // rather than queueing, we maintain our own single-threaded request stream. + // todo: we probably want retry logic here. + api.Perform(request); + + // Create a new request task if there's still more beatmaps to query. + lock (taskAssignmentLock) + { + pendingRequestTask = null; + if (pendingBeatmapTasks.Count > 0) + createNewTask(); + } + + List foundBeatmaps = new List { request.Response }; + + foreach (var beatmap in foundBeatmaps) + { + if (beatmapTasks.TryGetValue(beatmap.OnlineID, out var tasks)) + { + foreach (var task in tasks) + task.SetResult(beatmap); + + beatmapTasks.Remove(beatmap.OnlineID); + } + } + + // if any tasks remain which were not satisfied, return null. + foreach (var tasks in beatmapTasks.Values) + { + foreach (var task in tasks) + task.SetResult(null); + } + } + + private void createNewTask() => pendingRequestTask = Task.Run(performLookup); + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 6eb67b34e8..34344f8022 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -142,6 +142,7 @@ namespace osu.Game private BeatmapDifficultyCache difficultyCache; private UserLookupCache userCache; + private BeatmapLookupCache beatmapCache; private FileStore fileStore; @@ -265,6 +266,9 @@ namespace osu.Game dependencies.Cache(userCache = new UserLookupCache()); AddInternal(userCache); + dependencies.Cache(beatmapCache = new BeatmapLookupCache()); + AddInternal(beatmapCache); + var scorePerformanceManager = new ScorePerformanceCache(); dependencies.Cache(scorePerformanceManager); AddInternal(scorePerformanceManager); From f58c5cd9c0f2d9a6ffaf0457d11cb62cc124249e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 19:36:25 +0900 Subject: [PATCH 075/419] Update `MultiplayerClient` to use `BeatmapLookupCache` --- .../Online/Multiplayer/MultiplayerClient.cs | 20 +++++--------- .../Multiplayer/OnlineMultiplayerClient.cs | 27 +++++-------------- .../Multiplayer/TestMultiplayerClient.cs | 15 ++++++----- 3 files changed, 20 insertions(+), 42 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 4c472164d6..aa91d5e56f 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -702,15 +702,7 @@ namespace osu.Game.Online.Multiplayer private async Task createPlaylistItem(MultiplayerPlaylistItem item) { - var set = await GetOnlineBeatmapSet(item.BeatmapID).ConfigureAwait(false); - - // The incoming response is deserialised without circular reference handling currently. - // Because we require using metadata from this instance, populate the nested beatmaps' sets manually here. - foreach (var b in set.Beatmaps) - b.BeatmapSet = set; - - var beatmap = set.Beatmaps.Single(b => b.OnlineID == item.BeatmapID); - beatmap.Checksum = item.BeatmapChecksum; + var apiBeatmap = await GetOnlineBeatmapSet(item.BeatmapID).ConfigureAwait(false); var ruleset = Rulesets.GetRuleset(item.RulesetID); var rulesetInstance = ruleset.CreateInstance(); @@ -719,7 +711,7 @@ namespace osu.Game.Online.Multiplayer { ID = item.ID, OwnerID = item.OwnerID, - Beatmap = { Value = beatmap }, + Beatmap = { Value = apiBeatmap }, Ruleset = { Value = ruleset }, Expired = item.Expired }; @@ -731,12 +723,12 @@ namespace osu.Game.Online.Multiplayer } /// - /// Retrieves a from an online source. + /// Retrieves a from an online source. /// - /// The beatmap set ID. + /// The beatmap ID. /// A token to cancel the request. - /// The retrieval task. - protected abstract Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default); + /// The retrieval task. + protected abstract Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default); /// /// For the provided user ID, update whether the user is included in . diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 7308c03ec3..de974cb3e1 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -9,8 +9,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; using osu.Framework.Allocation; using osu.Framework.Bindables; +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.Rooms; @@ -29,6 +29,9 @@ namespace osu.Game.Online.Multiplayer private HubConnection? connection => connector?.CurrentConnection; + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + public OnlineMultiplayerClient(EndpointConfiguration endpoints) { endpoint = endpoints.MultiplayerEndpointUrl; @@ -159,27 +162,9 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.AddPlaylistItem), item); } - protected override Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default) + protected override Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default) { - var tcs = new TaskCompletionSource(); - var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId); - - req.Success += res => - { - if (cancellationToken.IsCancellationRequested) - { - tcs.SetCanceled(); - return; - } - - tcs.SetResult(res); - }; - - req.Failure += e => tcs.SetException(e); - - API.Queue(req); - - return tcs.Task; + return beatmapLookupCache.GetBeatmapAsync(beatmapId, cancellationToken); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 2d77e17513..39199ae304 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -336,7 +336,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task AddPlaylistItem(MultiplayerPlaylistItem item) => AddUserPlaylistItem(api.LocalUser.Value.OnlineID, item); - protected override Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default) + protected override Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default) { IBeatmapSetInfo? set = roomManager.ServerSideRooms.SelectMany(r => r.Playlist) .FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value.BeatmapSet @@ -345,13 +345,14 @@ namespace osu.Game.Tests.Visual.Multiplayer if (set == null) throw new InvalidOperationException("Beatmap not found."); - var apiSet = new APIBeatmapSet + return Task.FromResult(new APIBeatmap { - OnlineID = set.OnlineID, - Beatmaps = set.Beatmaps.Select(b => new APIBeatmap { OnlineID = b.OnlineID }).ToArray(), - }; - - return Task.FromResult(apiSet); + BeatmapSet = new APIBeatmapSet + { + OnlineID = set.OnlineID, + }, + OnlineID = set.Beatmaps.First().OnlineID + }); } private async Task changeMatchType(MatchType type) From 01bc330d1ce8468cecd95f83bca02be187730867 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 19:42:46 +0900 Subject: [PATCH 076/419] Rename method to match new purpose --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 4 ++-- osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs | 2 +- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index aa91d5e56f..478da983cd 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -702,7 +702,7 @@ namespace osu.Game.Online.Multiplayer private async Task createPlaylistItem(MultiplayerPlaylistItem item) { - var apiBeatmap = await GetOnlineBeatmapSet(item.BeatmapID).ConfigureAwait(false); + var apiBeatmap = await GetAPIBeatmap(item.BeatmapID).ConfigureAwait(false); var ruleset = Rulesets.GetRuleset(item.RulesetID); var rulesetInstance = ruleset.CreateInstance(); @@ -728,7 +728,7 @@ namespace osu.Game.Online.Multiplayer /// The beatmap ID. /// A token to cancel the request. /// The retrieval task. - protected abstract Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default); + protected abstract Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default); /// /// For the provided user ID, update whether the user is included in . diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index de974cb3e1..41687b54b0 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -162,7 +162,7 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.AddPlaylistItem), item); } - protected override Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default) + protected override Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default) { return beatmapLookupCache.GetBeatmapAsync(beatmapId, cancellationToken); } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 39199ae304..037dccc63f 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -336,7 +336,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task AddPlaylistItem(MultiplayerPlaylistItem item) => AddUserPlaylistItem(api.LocalUser.Value.OnlineID, item); - protected override Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default) + protected override Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default) { IBeatmapSetInfo? set = roomManager.ServerSideRooms.SelectMany(r => r.Playlist) .FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value.BeatmapSet From 23dd21339ddf8fd973817e6c19fe18fcdd8b3156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Nov 2021 13:39:09 +0100 Subject: [PATCH 077/419] Delay online fetch of non-current playlist item on room join --- .../Online/Multiplayer/MultiplayerClient.cs | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 478da983cd..4b44d5724f 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -139,7 +139,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(joinedRoom != null); // Populate playlist items. - var playlistItems = await Task.WhenAll(joinedRoom.Playlist.Select(createPlaylistItem)).ConfigureAwait(false); + var playlistItems = await Task.WhenAll(joinedRoom.Playlist.Select(item => createPlaylistItem(item, item.ID == joinedRoom.Settings.PlaylistItemId))).ConfigureAwait(false); // Populate users. Debug.Assert(joinedRoom.Users != null); @@ -605,7 +605,7 @@ namespace osu.Game.Online.Multiplayer if (Room == null) return; - var playlistItem = await createPlaylistItem(item).ConfigureAwait(false); + var playlistItem = await createPlaylistItem(item, true).ConfigureAwait(false); Scheduler.Add(() => { @@ -647,7 +647,7 @@ namespace osu.Game.Online.Multiplayer if (Room == null) return; - var playlistItem = await createPlaylistItem(item).ConfigureAwait(false); + var playlistItem = await createPlaylistItem(item, true).ConfigureAwait(false); Scheduler.Add(() => { @@ -700,10 +700,8 @@ namespace osu.Game.Online.Multiplayer CurrentMatchPlayingItem.Value = APIRoom.Playlist.SingleOrDefault(p => p.ID == settings.PlaylistItemId); } - private async Task createPlaylistItem(MultiplayerPlaylistItem item) + private async Task createPlaylistItem(MultiplayerPlaylistItem item, bool populateImmediately) { - var apiBeatmap = await GetAPIBeatmap(item.BeatmapID).ConfigureAwait(false); - var ruleset = Rulesets.GetRuleset(item.RulesetID); var rulesetInstance = ruleset.CreateInstance(); @@ -711,7 +709,6 @@ namespace osu.Game.Online.Multiplayer { ID = item.ID, OwnerID = item.OwnerID, - Beatmap = { Value = apiBeatmap }, Ruleset = { Value = ruleset }, Expired = item.Expired }; @@ -719,9 +716,25 @@ namespace osu.Game.Online.Multiplayer playlistItem.RequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))); playlistItem.AllowedMods.AddRange(item.AllowedMods.Select(m => m.ToMod(rulesetInstance))); + if (populateImmediately) + { + await populateFromOnline(item, playlistItem).ConfigureAwait(false); + } + else + { + // to avoid blocking other operations (like the initial room join), schedule online population to happen in the background. + // ReSharper disable once AsyncVoidLambda + Schedule(async () => await populateFromOnline(item, playlistItem).ConfigureAwait(false)); + } + return playlistItem; } + private async Task populateFromOnline(MultiplayerPlaylistItem item, PlaylistItem playlistItem) + { + playlistItem.Beatmap.Value = await GetAPIBeatmap(item.BeatmapID).ConfigureAwait(false); + } + /// /// Retrieves a from an online source. /// From 695167a7493e305e0e1f011de1750105561d65a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Nov 2021 13:45:29 +0100 Subject: [PATCH 078/419] Add support for null item display in `DrawableRoomPlaylistItem` --- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 2dbe2df82c..79a624b884 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -50,6 +50,7 @@ namespace osu.Game.Screens.OnlinePlay private LinkFlowContainer authorText; private ExplicitContentBeatmapPill explicitContentPill; private ModDisplay modDisplay; + private FillFlowContainer buttonsFlow; private UpdateableAvatar ownerAvatar; private readonly IBindable valid = new Bindable(); @@ -150,15 +151,22 @@ namespace osu.Game.Screens.OnlinePlay .ContinueWith(u => Schedule(() => ownerAvatar.User = u.Result), TaskContinuationOptions.OnlyOnRanToCompletion); } - difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset.Value, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(ICON_HEIGHT) }; + if (Item.Beatmap.Value != null) + difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset.Value, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(ICON_HEIGHT) }; + else + difficultyIconContainer.Clear(); panelBackground.Beatmap.Value = Item.Beatmap.Value; beatmapText.Clear(); - beatmapText.AddLink(Item.Beatmap.Value.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineID.ToString(), null, text => + + if (Item.Beatmap.Value != null) { - text.Truncate = true; - }); + beatmapText.AddLink(Item.Beatmap.Value.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineID.ToString(), null, text => + { + text.Truncate = true; + }); + } authorText.Clear(); @@ -168,10 +176,13 @@ namespace osu.Game.Screens.OnlinePlay authorText.AddUserLink(Item.Beatmap.Value.Metadata.Author); } - bool hasExplicitContent = (Item.Beatmap.Value.BeatmapSet as IBeatmapSetOnlineInfo)?.HasExplicitContent == true; + bool hasExplicitContent = (Item.Beatmap.Value?.BeatmapSet as IBeatmapSetOnlineInfo)?.HasExplicitContent == true; explicitContentPill.Alpha = hasExplicitContent ? 1 : 0; modDisplay.Current.Value = requiredMods.ToArray(); + + buttonsFlow.Clear(); + buttonsFlow.ChildrenEnumerable = CreateButtons(); } protected override Drawable CreateContent() @@ -273,7 +284,7 @@ namespace osu.Game.Screens.OnlinePlay } } }, - new FillFlowContainer + buttonsFlow = new FillFlowContainer { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, @@ -305,9 +316,9 @@ namespace osu.Game.Screens.OnlinePlay } protected virtual IEnumerable CreateButtons() => - new Drawable[] + new[] { - new PlaylistDownloadButton(Item), + Item.Beatmap.Value == null ? Empty() : new PlaylistDownloadButton(Item), new PlaylistRemoveButton { Size = new Vector2(30, 30), From 95373649a4440067f424bb30226153a5f48aa378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Nov 2021 14:02:47 +0100 Subject: [PATCH 079/419] Skip null items in star rating range display --- .../Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs index fc029543bb..edf9c5d155 100644 --- a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs @@ -80,7 +80,7 @@ namespace osu.Game.Screens.OnlinePlay.Components private void updateRange(object sender, NotifyCollectionChangedEventArgs e) { - var orderedDifficulties = Playlist.Select(p => p.Beatmap.Value).OrderBy(b => b.StarRating).ToArray(); + var orderedDifficulties = Playlist.Where(p => p.Beatmap.Value != null).Select(p => p.Beatmap.Value).OrderBy(b => b.StarRating).ToArray(); StarDifficulty minDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[0].StarRating : 0, 0); StarDifficulty maxDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[^1].StarRating : 0, 0); From fe119da044a3290df438908da7d82a7ecfe879b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 19:52:36 +0900 Subject: [PATCH 080/419] Add fetching of beatmap and user data when playlist panels come on screen --- .../Online/Multiplayer/MultiplayerClient.cs | 34 +++++++++-------- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 37 ++++++++++++++----- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 4b44d5724f..78bf2c4db3 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -444,10 +444,18 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } - Task IMultiplayerClient.SettingsChanged(MultiplayerRoomSettings newSettings) + async Task IMultiplayerClient.SettingsChanged(MultiplayerRoomSettings newSettings) { + Debug.Assert(APIRoom != null); + Debug.Assert(Room != null); + + // ensure the new selected item is populated immediately. + var playlistItem = APIRoom.Playlist.SingleOrDefault(p => p.ID == newSettings.PlaylistItemId); + + if (playlistItem != null) + await PopulateBeatmap(playlistItem).ConfigureAwait(false); + Scheduler.Add(() => updateLocalRoomSettings(newSettings)); - return Task.CompletedTask; } Task IMultiplayerClient.UserStateChanged(int userId, MultiplayerUserState state) @@ -700,7 +708,7 @@ namespace osu.Game.Online.Multiplayer CurrentMatchPlayingItem.Value = APIRoom.Playlist.SingleOrDefault(p => p.ID == settings.PlaylistItemId); } - private async Task createPlaylistItem(MultiplayerPlaylistItem item, bool populateImmediately) + private async Task createPlaylistItem(MultiplayerPlaylistItem item, bool populateBeatmapImmediately) { var ruleset = Rulesets.GetRuleset(item.RulesetID); var rulesetInstance = ruleset.CreateInstance(); @@ -708,6 +716,7 @@ namespace osu.Game.Online.Multiplayer var playlistItem = new PlaylistItem { ID = item.ID, + BeatmapID = item.BeatmapID, OwnerID = item.OwnerID, Ruleset = { Value = ruleset }, Expired = item.Expired @@ -716,23 +725,18 @@ namespace osu.Game.Online.Multiplayer playlistItem.RequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))); playlistItem.AllowedMods.AddRange(item.AllowedMods.Select(m => m.ToMod(rulesetInstance))); - if (populateImmediately) - { - await populateFromOnline(item, playlistItem).ConfigureAwait(false); - } - else - { - // to avoid blocking other operations (like the initial room join), schedule online population to happen in the background. - // ReSharper disable once AsyncVoidLambda - Schedule(async () => await populateFromOnline(item, playlistItem).ConfigureAwait(false)); - } + if (populateBeatmapImmediately) + await PopulateBeatmap(playlistItem).ConfigureAwait(false); return playlistItem; } - private async Task populateFromOnline(MultiplayerPlaylistItem item, PlaylistItem playlistItem) + public async Task PopulateBeatmap(PlaylistItem item) { - playlistItem.Beatmap.Value = await GetAPIBeatmap(item.BeatmapID).ConfigureAwait(false); + if (item.Beatmap.Value != null) + return; + + item.Beatmap.Value = await GetAPIBeatmap(item.BeatmapID).ConfigureAwait(false); } /// diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 79a624b884..737c331d2f 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Database; @@ -24,6 +25,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osu.Game.Online.Chat; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays.BeatmapSet; using osu.Game.Rulesets; @@ -67,6 +69,13 @@ namespace osu.Game.Screens.OnlinePlay [Resolved] private UserLookupCache userLookupCache { get; set; } + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } + + private PanelBackground panelBackground; + + private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty); + private readonly bool allowEdit; private readonly bool allowSelection; private readonly bool showItemOwner; @@ -131,11 +140,27 @@ namespace osu.Game.Screens.OnlinePlay valid.BindValueChanged(_ => Scheduler.AddOnce(refresh)); requiredMods.CollectionChanged += (_, __) => Scheduler.AddOnce(refresh); + onScreenLoader.DelayedLoadStarted += _ => + { + Task.Run(async () => + { + try + { + var user = await userLookupCache.GetUserAsync(Item.OwnerID).ConfigureAwait(false); + Schedule(() => ownerAvatar.User = user); + + await multiplayerClient.PopulateBeatmap(Item).ConfigureAwait(false); + } + catch (Exception e) + { + Logger.Log($"Error while populating playlist item {e}"); + } + }); + }; + refresh(); } - private PanelBackground panelBackground; - private void refresh() { if (!valid.Value) @@ -144,13 +169,6 @@ namespace osu.Game.Screens.OnlinePlay maskingContainer.BorderColour = colours.Red; } - if (showItemOwner) - { - ownerAvatar.Show(); - userLookupCache.GetUserAsync(Item.OwnerID) - .ContinueWith(u => Schedule(() => ownerAvatar.User = u.Result), TaskContinuationOptions.OnlyOnRanToCompletion); - } - if (Item.Beatmap.Value != null) difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset.Value, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(ICON_HEIGHT) }; else @@ -203,6 +221,7 @@ namespace osu.Game.Screens.OnlinePlay Alpha = 0, AlwaysPresent = true }, + onScreenLoader, panelBackground = new PanelBackground { RelativeSizeAxes = Axes.Both, From e4ba66877d95c6ed45d2fd9f9204ee4a7058ed7c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 19:54:23 +0900 Subject: [PATCH 081/419] Improve transitions when loading new data into a playlist panel --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 737c331d2f..e4b0d9647e 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -80,6 +80,8 @@ namespace osu.Game.Screens.OnlinePlay private readonly bool allowSelection; private readonly bool showItemOwner; + private FillFlowContainer mainFillFlow; + protected override bool ShouldBeConsideredForInput(Drawable child) => allowEdit || !allowSelection || SelectedItem.Value == Model; public DrawableRoomPlaylistItem(PlaylistItem item, bool allowEdit, bool allowSelection, bool showItemOwner) @@ -201,6 +203,9 @@ namespace osu.Game.Screens.OnlinePlay buttonsFlow.Clear(); buttonsFlow.ChildrenEnumerable = CreateButtons(); + + difficultyIconContainer.FadeInFromZero(500, Easing.OutQuint); + mainFillFlow.FadeInFromZero(500, Easing.OutQuint); } protected override Drawable CreateContent() @@ -247,7 +252,7 @@ namespace osu.Game.Screens.OnlinePlay AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Left = 8, Right = 8 }, }, - new FillFlowContainer + mainFillFlow = new FillFlowContainer { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, From 0fae10500a2860933f7125da02b1c4c2e09d80b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Nov 2021 20:02:15 +0900 Subject: [PATCH 082/419] Fix failing tests --- .../Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 037dccc63f..05b7c11e34 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -347,11 +347,9 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.FromResult(new APIBeatmap { - BeatmapSet = new APIBeatmapSet - { - OnlineID = set.OnlineID, - }, - OnlineID = set.Beatmaps.First().OnlineID + BeatmapSet = new APIBeatmapSet { OnlineID = set.OnlineID }, + OnlineID = beatmapId, + Checksum = set.Beatmaps.First(b => b.OnlineID == beatmapId).MD5Hash }); } From d18417aa2706d1eae6ac1094d942d4538372c0e9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 23 Nov 2021 12:04:27 +0300 Subject: [PATCH 083/419] Enable build-only iOS CI --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68f8ef51ef..3c52802cf6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,10 +77,6 @@ jobs: run: msbuild osu.Android/osu.Android.csproj /restore /p:Configuration=Debug build-only-ios: - # While this workflow technically *can* run, it fails as iOS builds are blocked by multiple issues. - # See https://github.com/ppy/osu-framework/issues/4677 for the details. - # The job can be unblocked once those issues are resolved and game deployments can happen again. - if: false name: Build only (iOS) runs-on: macos-latest timeout-minutes: 60 From 03e1305b3f54639fd4375d715faaf7257c8adec2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 01:55:14 +0900 Subject: [PATCH 084/419] Fix toast display potentially causing a child mutation before load complete --- osu.Game/Overlays/OnScreenDisplay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/OnScreenDisplay.cs b/osu.Game/Overlays/OnScreenDisplay.cs index af6d24fc65..be9d3cd794 100644 --- a/osu.Game/Overlays/OnScreenDisplay.cs +++ b/osu.Game/Overlays/OnScreenDisplay.cs @@ -95,13 +95,13 @@ namespace osu.Game.Overlays /// Displays the provided temporarily. /// /// - public void Display(Toast toast) + public void Display(Toast toast) => Schedule(() => { box.Child = toast; DisplayTemporarily(box); - } + }); - private void displayTrackedSettingChange(SettingDescription description) => Schedule(() => Display(new TrackedSettingToast(description))); + private void displayTrackedSettingChange(SettingDescription description) => Display(new TrackedSettingToast(description)); private TransformSequence fadeIn; private ScheduledDelegate fadeOut; From dda7142f48226f9db18e96efd8ce06b1fb286f47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 02:06:40 +0900 Subject: [PATCH 085/419] Fix double screen exit in multiplayer song select tests potentially causing failure --- .../Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 84b24ba3a1..a5229702a8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -144,7 +144,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set mods", () => SelectedMods.Value = new[] { new TaikoModDoubleTime() }); AddStep("confirm selection", () => songSelect.FinaliseSelection()); - AddStep("exit song select", () => songSelect.Exit()); + + AddUntilStep("song select exited", () => !songSelect.IsCurrentScreen()); AddAssert("beatmap not changed", () => Beatmap.Value.BeatmapInfo.Equals(selectedBeatmap)); AddAssert("ruleset not changed", () => Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo)); From 9411b42d0a68c3ca6123da8843ea64d4415e4718 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 02:27:41 +0900 Subject: [PATCH 086/419] Cache skin filename to path mapping to reduce realm lookups during gameplay skin changes --- .../LegacyDatabasedSkinResourceStore.cs | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs b/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs index ab820a13ab..4194aa5089 100644 --- a/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs +++ b/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs @@ -3,10 +3,8 @@ using System; using System.Collections.Generic; -using System.Linq; using osu.Framework.Extensions; using osu.Framework.IO.Stores; -using osu.Game.Database; using osu.Game.Extensions; using Realms; @@ -14,12 +12,36 @@ namespace osu.Game.Skinning { public class LegacyDatabasedSkinResourceStore : ResourceStore { - private readonly ILive source; + private readonly Dictionary fileToStoragePathMapping = new Dictionary(); + + private readonly IDisposable subscription; public LegacyDatabasedSkinResourceStore(SkinInfo source, IResourceStore underlyingStore) : base(underlyingStore) { - this.source = source.ToLive(); + subscription = source.Files.SubscribeForNotifications((sender, changes, error) => + { + if (changes == null) + return; + + // If a large number of changes are made on skin files, this may be better suited to being cleared here + // and reinitialised on next usage. + initialiseFileCache(source); + }); + + initialiseFileCache(source); + } + + ~LegacyDatabasedSkinResourceStore() + { + Dispose(false); + } + + private void initialiseFileCache(SkinInfo source) + { + fileToStoragePathMapping.Clear(); + foreach (var f in source.Files) + fileToStoragePathMapping[f.Filename.ToLowerInvariant()] = f.File.GetStoragePath(); } protected override IEnumerable GetFilenames(string name) @@ -32,18 +54,16 @@ namespace osu.Game.Skinning } } + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + subscription?.Dispose(); + } + private string getPathForFile(string filename) => - source.PerformRead(s => - { - if (s.IsManaged) - { - // avoid enumerating all files if this is a managed realm instance. - return s.Files.Filter(@"Filename ==[c] $0", filename).FirstOrDefault()?.File.GetStoragePath(); - } + fileToStoragePathMapping.TryGetValue(filename.ToLower(), out string path) ? path : null; - return s.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath(); - }); - - public override IEnumerable GetAvailableResources() => source.PerformRead(s => s.Files.Select(f => f.Filename)); + public override IEnumerable GetAvailableResources() => fileToStoragePathMapping.Keys; } } From b74b09eb3a7f18634bc84696ba9a57dbca0f7f1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Nov 2021 20:56:11 +0100 Subject: [PATCH 087/419] Add extra until step to make cause of potential failures more obvious --- osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index e00939a4de..ab9dc9c989 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -172,6 +172,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); AddWaitStep("wait for transition", 2); + AddUntilStep("create room button enabled", () => this.ChildrenOfType().Single().Enabled.Value); AddStep("create room", () => { InputManager.MoveMouseTo(this.ChildrenOfType().Single()); From de034b4d9c93d09e9ce4695fbccf13fe648f9414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Nov 2021 20:57:02 +0100 Subject: [PATCH 088/419] Fix test failures due to wrong asserts & uninitialised playlist * The "create room" button was disabled headless due to not specifying the imported beatmap. In visual tests it seems to work due to selecting from the local database randomly. * The test asserts are brought in line with expectations. --- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index ab9dc9c989..981989c28a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -125,17 +125,25 @@ namespace osu.Game.Tests.Visual.Multiplayer createRoom(() => new Room { Name = { Value = "Test Room" }, - Type = { Value = MatchType.TeamVersus } + Type = { Value = MatchType.HeadToHead }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } }); - AddUntilStep("match type versus", () => client.APIRoom?.Type.Value == MatchType.HeadToHead); + AddUntilStep("match type head to head", () => client.APIRoom?.Type.Value == MatchType.HeadToHead); AddStep("change match type", () => client.ChangeSettings(new MultiplayerRoomSettings { MatchType = MatchType.TeamVersus })); - AddUntilStep("api room updated", () => client.APIRoom?.Type.Value == MatchType.TeamVersus); + AddUntilStep("api room updated to team versus", () => client.APIRoom?.Type.Value == MatchType.TeamVersus); } [Test] From 0479027f644c686cf65d3ee9ceb65e14bd0e42e4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 11:29:02 +0900 Subject: [PATCH 089/419] Apply assertion fix from review --- .../DifficultyAdjustmentModCombinationsTest.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index 612927337f..ae8eec2629 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -142,16 +142,16 @@ namespace osu.Game.Tests.NonVisual { Assert.AreEqual(expectedCombinations.Length, actualCombinations.Length); - foreach (Type[] expected in expectedCombinations) + Assert.Multiple(() => { - Type[] actualTypes = ModUtils.FlattenMods(actualCombinations).Select(m => m.GetType()).ToArray(); - - Assert.Multiple(() => + for (int i = 0; i < expectedCombinations.Length; ++i) { - foreach (var expectedType in expected) - Assert.Contains(expectedType, actualTypes); - }); - } + Type[] expectedTypes = expectedCombinations[i]; + Type[] actualTypes = ModUtils.FlattenMod(actualCombinations[i]).Select(m => m.GetType()).ToArray(); + + Assert.That(expectedTypes, Is.EquivalentTo(actualTypes)); + } + }); } private class ModA : Mod From d78c18d03f6982f260ef717f53357d2db823d6e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 12:44:25 +0900 Subject: [PATCH 090/419] Remove excess brackets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index 9b7e169745..f3c2cd8ba2 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -268,7 +268,7 @@ namespace osu.Game.Tests.Skins.IO private async Task> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null) { var skinManager = osu.Dependencies.Get(); - return (await skinManager.Import(archive)); + return await skinManager.Import(archive); } } } From fb2310f82695f146a3c8dfb1a8ebb176ed7fe5b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 12:45:47 +0900 Subject: [PATCH 091/419] Specify config file default as `DEFAULT_SKIN`'s guid --- osu.Game/Configuration/OsuConfigManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index ade817e652..c6a2abecd7 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -17,6 +17,7 @@ using osu.Game.Overlays; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; +using osu.Game.Skinning; namespace osu.Game.Configuration { @@ -27,7 +28,7 @@ namespace osu.Game.Configuration { // UI/selection defaults SetDefault(OsuSetting.Ruleset, string.Empty); - SetDefault(OsuSetting.Skin, string.Empty); + SetDefault(OsuSetting.Skin, SkinInfo.DEFAULT_SKIN.ToString()); SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details); SetDefault(OsuSetting.BeatmapDetailModsFilter, false); From 370135d484bd1bece6925fe577a9074cfaa8e163 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 12:47:32 +0900 Subject: [PATCH 092/419] Remove outdated TODO --- osu.Game/OsuGame.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 8af76c1289..047c3b4225 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -247,7 +247,6 @@ namespace osu.Game SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString(); configSkin.ValueChanged += skinId => { - // TODO: migrate the user skin selection to the new ID format. ILive skinInfo = null; if (Guid.TryParse(skinId.NewValue, out var guid)) From 0e0e8c25e8587e0fd121d1c4a0ad8cfe9055a08f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 12:48:49 +0900 Subject: [PATCH 093/419] Refactor migration precondition to read better --- osu.Game/Database/EFToRealmMigrator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index 670eb95ad6..8f436470af 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -63,7 +63,7 @@ namespace osu.Game.Database using (var transaction = realm.BeginWrite()) { // only migrate data if the realm database is empty. - if (!realm.All().Any(s => !s.Protected)) + if (realm.All().All(s => s.Protected)) { foreach (var skin in existingSkins) { From 8ce7467e9784f7ebd1a0f6f75fcf437e87832d1c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 12:50:47 +0900 Subject: [PATCH 094/419] Fix ordering of skins in dropdown being reverse of expected --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 4de5d455fe..e3230098e2 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -81,7 +81,7 @@ namespace osu.Game.Overlays.Settings.Sections realmSkins = realmFactory.Context.All() .Where(s => !s.DeletePending) - .OrderBy(s => s.Protected) + .OrderByDescending(s => s.Protected) // protected skins should be at the top. .ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase); realmSubscription = realmSkins From ea66cd6c5e3c31602cec5a9d03d54a9d24cb411d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 12:55:19 +0900 Subject: [PATCH 095/419] Add xmldoc and make realm ongoing transaction file op methods `protected` instead of `public` --- osu.Game/Stores/RealmArchiveModelManager.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index ae40bcac79..a916e2b53a 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -57,17 +57,26 @@ namespace osu.Game.Stores public void AddFile(TModel item, Stream stream, string filename) => item.Realm.Write(() => AddFile(item, stream, filename, item.Realm)); - public void DeleteFile(TModel item, RealmNamedFileUsage file, Realm realm) + /// + /// Delete a file from within an ongoing realm transaction. + /// + protected void DeleteFile(TModel item, RealmNamedFileUsage file, Realm realm) { item.Files.Remove(file); } - public void ReplaceFile(TModel model, RealmNamedFileUsage file, Stream contents, Realm realm) + /// + /// Replace a file from within an ongoing realm transaction. + /// + protected void ReplaceFile(TModel model, RealmNamedFileUsage file, Stream contents, Realm realm) { file.File = realmFileStore.Add(contents, realm); } - public void AddFile(TModel item, Stream stream, string filename, Realm realm) + /// + /// Add a file from within an ongoing realm transaction. + /// + protected void AddFile(TModel item, Stream stream, string filename, Realm realm) { var file = realmFileStore.Add(stream, realm); var namedUsage = new RealmNamedFileUsage(file, filename); From 1cf15438664b5271f8ebf8c15bc201093c8da468 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 13:50:19 +0900 Subject: [PATCH 096/419] Fix test failures due to attempting to subscribe to non-managed instances --- .../LegacyDatabasedSkinResourceStore.cs | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs b/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs index 4194aa5089..667dc1876b 100644 --- a/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs +++ b/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using osu.Framework.Development; using osu.Framework.Extensions; using osu.Framework.IO.Stores; using osu.Game.Extensions; @@ -19,15 +21,24 @@ namespace osu.Game.Skinning public LegacyDatabasedSkinResourceStore(SkinInfo source, IResourceStore underlyingStore) : base(underlyingStore) { - subscription = source.Files.SubscribeForNotifications((sender, changes, error) => + // Subscribing to non-managed instances doesn't work. + // In this usage, the skin may be non-managed in tests. + if (source.IsManaged) { - if (changes == null) - return; + // Subscriptions can only work on the main thread. + Debug.Assert(ThreadSafety.IsUpdateThread); - // If a large number of changes are made on skin files, this may be better suited to being cleared here - // and reinitialised on next usage. - initialiseFileCache(source); - }); + subscription = source.Files + .AsRealmCollection().SubscribeForNotifications((sender, changes, error) => + { + if (changes == null) + return; + + // If a large number of changes are made on skin files, this may be better suited to being cleared here + // and reinitialised on next usage. + initialiseFileCache(source); + }); + } initialiseFileCache(source); } From 4306420922ef927a88101f1322b3cc5b181ce2ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 15:09:51 +0900 Subject: [PATCH 097/419] Add extension methods to add extra safety to realm subscriptions Also adjusts the naming and documentation to make it (hopefully) easier to understand what this method/process implies. --- osu.Game.Tests/Database/GeneralUsageTests.cs | 6 +- osu.Game.Tests/Database/RealmLiveTests.cs | 2 +- osu.Game/Database/RealmObjectExtensions.cs | 106 ++++++++++++++++++ .../Bindings/DatabasedKeyBindingContainer.cs | 3 +- 4 files changed, 111 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index 841bf2de43..2285b22a3a 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -5,8 +5,8 @@ using System; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; +using osu.Game.Database; using osu.Game.Models; -using Realms; #nullable enable @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Database using (var context = realmFactory.CreateContext()) { - var subscription = context.All().SubscribeForNotifications((sender, changes, error) => + var subscription = context.All().QueryAsyncWithNotifications((sender, changes, error) => { using (realmFactory.CreateContext()) { @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Database { } - subscription.Dispose(); + subscription?.Dispose(); } Assert.IsTrue(callbackRan); diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 8ab19c8329..9b6769b788 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -208,7 +208,7 @@ namespace osu.Game.Tests.Database using (var updateThreadContext = realmFactory.CreateContext()) { - updateThreadContext.All().SubscribeForNotifications(gotChange); + updateThreadContext.All().QueryAsyncWithNotifications(gotChange); ILive? liveBeatmap = null; Task.Factory.StartNew(() => diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index ac4ca436ad..170756e743 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -1,12 +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 System; using System.Collections.Generic; using System.Linq; using AutoMapper; +using osu.Framework.Development; using osu.Game.Input.Bindings; using Realms; +#nullable enable + namespace osu.Game.Database { public static class RealmObjectExtensions @@ -60,5 +64,107 @@ namespace osu.Game.Database { return new RealmLive(realmObject); } + + /// + /// Register a callback to be invoked each time this changes. + /// + /// + /// + /// This adds osu! specific thread and managed state safety checks on top of . + /// + /// + /// The first callback will be invoked with the initial after the asynchronous query completes, + /// and then called again after each write transaction which changes either any of the objects in the collection, or + /// which objects are in the collection. The changes parameter will + /// be null the first time the callback is invoked with the initial results. For each call after that, + /// it will contain information about which rows in the results were added, removed or modified. + /// + /// + /// If a write transaction did not modify any objects in this , the callback is not invoked at all. + /// If an error occurs the callback will be invoked with null for the sender parameter and a non-null error. + /// Currently the only errors that can occur are when opening the on the background worker thread. + /// + /// + /// At the time when the block is called, the object will be fully evaluated + /// and up-to-date, and as long as you do not perform a write transaction on the same thread + /// or explicitly call , accessing it will never perform blocking work. + /// + /// + /// Notifications are delivered via the standard event loop, and so can't be delivered while the event loop is blocked by other activity. + /// When notifications can't be delivered instantly, multiple notifications may be coalesced into a single notification. + /// This can include the notification with the initial collection. + /// + /// + /// The to observe for changes. + /// The callback to be invoked with the updated . + /// + /// A subscription token. It must be kept alive for as long as you want to receive change notifications. + /// To stop receiving notifications, call . + /// + /// + /// + public static IDisposable? QueryAsyncWithNotifications(this IRealmCollection collection, NotificationCallbackDelegate callback) + where T : RealmObjectBase + { + // Subscriptions can only work on the main thread. + if (!ThreadSafety.IsUpdateThread) + throw new InvalidOperationException("Cannot subscribe for realm notifications from a non-update thread."); + + return collection.SubscribeForNotifications(callback); + } + + /// + /// A convenience method that casts to and subscribes for change notifications. + /// + /// + /// This adds osu! specific thread and managed state safety checks on top of . + /// + /// The to observe for changes. + /// Type of the elements in the list. + /// + /// The callback to be invoked with the updated . + /// + /// A subscription token. It must be kept alive for as long as you want to receive change notifications. + /// To stop receiving notifications, call . + /// + /// May be null in the case the provided collection is not managed. + /// + public static IDisposable? QueryAsyncWithNotifications(this IQueryable list, NotificationCallbackDelegate callback) + where T : RealmObjectBase + { + // Subscribing to non-managed instances doesn't work. + // In this usage, the skin may be non-managed in tests. + if (!(list is IRealmCollection realmCollection)) + return null; + + return QueryAsyncWithNotifications(realmCollection, callback); + } + + /// + /// A convenience method that casts to and subscribes for change notifications. + /// + /// + /// This adds osu! specific thread and managed state safety checks on top of . + /// + /// The to observe for changes. + /// Type of the elements in the list. + /// + /// The callback to be invoked with the updated . + /// + /// A subscription token. It must be kept alive for as long as you want to receive change notifications. + /// To stop receiving notifications, call . + /// + /// May be null in the case the provided collection is not managed. + /// + public static IDisposable? QueryAsyncWithNotifications(this IList list, NotificationCallbackDelegate callback) + where T : RealmObjectBase + { + // Subscribing to non-managed instances doesn't work. + // In this usage, the skin may be non-managed in tests. + if (!(list is IRealmCollection realmCollection)) + return null; + + return QueryAsyncWithNotifications(realmCollection, callback); + } } } diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index baa5b9ff9c..f95c884fe5 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Input.Bindings; using osu.Game.Database; using osu.Game.Rulesets; -using Realms; namespace osu.Game.Input.Bindings { @@ -56,7 +55,7 @@ namespace osu.Game.Input.Bindings .Where(b => b.RulesetName == rulesetName && b.Variant == variant); realmSubscription = realmKeyBindings - .SubscribeForNotifications((sender, changes, error) => + .QueryAsyncWithNotifications((sender, changes, error) => { // first subscription ignored as we are handling this in LoadComplete. if (changes == null) From a439209535ec8a439aab350225b1e6640c6e3cc6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 15:23:28 +0900 Subject: [PATCH 098/419] Add `BannedSymbols` rules for `SubscribeForNotifications` variants we use --- CodeAnalysis/BannedSymbols.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index b72803482d..c567adc0ae 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -10,3 +10,6 @@ T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods. T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods. M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast() instead. +M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection,NotificationCallbackDelegate) instead. +M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable,NotificationCallbackDelegate) instead. +M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList,NotificationCallbackDelegate) instead. From 09817ff161aa6768aa6fbf9828b62f572d075402 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 15:27:10 +0900 Subject: [PATCH 099/419] Add missing `returns` additional documentation to main method call --- osu.Game/Database/RealmObjectExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 170756e743..cb38c910da 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -100,6 +100,8 @@ namespace osu.Game.Database /// /// A subscription token. It must be kept alive for as long as you want to receive change notifications. /// To stop receiving notifications, call . + /// + /// May be null in the case the provided collection is not managed. /// /// /// From 81f82c24c39014decc49b676e68cca913db95645 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 17:45:41 +0900 Subject: [PATCH 100/419] Use new API endpoint to do batch lookups --- osu.Game/Database/BeatmapLookupCache.cs | 19 ++++++++------- .../Online/API/Requests/GetBeatmapsRequest.cs | 24 +++++++++++++++++++ .../API/Requests/GetBeatmapsResponse.cs | 15 ++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Online/API/Requests/GetBeatmapsRequest.cs create mode 100644 osu.Game/Online/API/Requests/GetBeatmapsResponse.cs diff --git a/osu.Game/Database/BeatmapLookupCache.cs b/osu.Game/Database/BeatmapLookupCache.cs index c4e20d59b6..2082031714 100644 --- a/osu.Game/Database/BeatmapLookupCache.cs +++ b/osu.Game/Database/BeatmapLookupCache.cs @@ -85,7 +85,7 @@ namespace osu.Game.Database // Grab at most 50 unique beatmap IDs from the queue. lock (taskAssignmentLock) { - while (pendingBeatmapTasks.Count > 0 && beatmapTasks.Count < 1) + while (pendingBeatmapTasks.Count > 0 && beatmapTasks.Count < 50) { (int id, TaskCompletionSource task) next = pendingBeatmapTasks.Dequeue(); @@ -103,7 +103,7 @@ namespace osu.Game.Database } // Query the beatmaps. - var request = new GetBeatmapRequest(new APIBeatmap { OnlineID = beatmapTasks.Keys.First() }); + var request = new GetBeatmapsRequest(beatmapTasks.Keys.ToArray()); // rather than queueing, we maintain our own single-threaded request stream. // todo: we probably want retry logic here. @@ -117,16 +117,19 @@ namespace osu.Game.Database createNewTask(); } - List foundBeatmaps = new List { request.Response }; + List foundBeatmaps = request.Response?.Beatmaps; - foreach (var beatmap in foundBeatmaps) + if (foundBeatmaps != null) { - if (beatmapTasks.TryGetValue(beatmap.OnlineID, out var tasks)) + foreach (var beatmap in foundBeatmaps) { - foreach (var task in tasks) - task.SetResult(beatmap); + if (beatmapTasks.TryGetValue(beatmap.OnlineID, out var tasks)) + { + foreach (var task in tasks) + task.SetResult(beatmap); - beatmapTasks.Remove(beatmap.OnlineID); + beatmapTasks.Remove(beatmap.OnlineID); + } } } diff --git a/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs new file mode 100644 index 0000000000..1d71e22b77 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Online.API.Requests +{ + public class GetBeatmapsRequest : APIRequest + { + private readonly int[] beatmapIds; + + private const int max_ids_per_request = 50; + + public GetBeatmapsRequest(int[] beatmapIds) + { + if (beatmapIds.Length > max_ids_per_request) + throw new ArgumentException($"{nameof(GetBeatmapsRequest)} calls only support up to {max_ids_per_request} IDs at once"); + + this.beatmapIds = beatmapIds; + } + + protected override string Target => "beatmaps/?ids[]=" + string.Join("&ids[]=", beatmapIds); + } +} diff --git a/osu.Game/Online/API/Requests/GetBeatmapsResponse.cs b/osu.Game/Online/API/Requests/GetBeatmapsResponse.cs new file mode 100644 index 0000000000..c450c3269c --- /dev/null +++ b/osu.Game/Online/API/Requests/GetBeatmapsResponse.cs @@ -0,0 +1,15 @@ +// 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 Newtonsoft.Json; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class GetBeatmapsResponse : ResponseWithCursor + { + [JsonProperty("beatmaps")] + public List Beatmaps; + } +} From bf5a186a2b03774d7f51dd6a591a7d529894f534 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 17:47:16 +0900 Subject: [PATCH 101/419] Add early abort to avoid sending empty lookup requests --- osu.Game/Database/BeatmapLookupCache.cs | 3 +++ osu.Game/Database/UserLookupCache.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/osu.Game/Database/BeatmapLookupCache.cs b/osu.Game/Database/BeatmapLookupCache.cs index 2082031714..c6f8244494 100644 --- a/osu.Game/Database/BeatmapLookupCache.cs +++ b/osu.Game/Database/BeatmapLookupCache.cs @@ -102,6 +102,9 @@ namespace osu.Game.Database } } + if (beatmapTasks.Count == 0) + return; + // Query the beatmaps. var request = new GetBeatmapsRequest(beatmapTasks.Keys.ToArray()); diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index dae2d2549c..26f4e9fb3b 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -100,6 +100,9 @@ namespace osu.Game.Database } } + if (userTasks.Count == 0) + return; + // Query the users. var request = new GetUsersRequest(userTasks.Keys.ToArray()); From 7224f6bac54127ad9c4559b02fb92eeb9d20277a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 20:00:31 +0900 Subject: [PATCH 102/419] Fix testable online IDs starting at 0 --- osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index abcf31c007..a4586dea12 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -25,9 +25,9 @@ namespace osu.Game.Tests.Visual.OnlinePlay private readonly List serverSideRooms = new List(); - private int currentRoomId; - private int currentPlaylistItemId; - private int currentScoreId; + private int currentRoomId = 1; + private int currentPlaylistItemId = 1; + private int currentScoreId = 1; /// /// Handles an API request, while also updating the local state to match From 685bdd522e1111749243e95b5dca854bd7064b31 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 20:17:26 +0900 Subject: [PATCH 103/419] Replace 'skin' in comments with 'instance' --- osu.Game/Database/RealmObjectExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index cb38c910da..b38e21453c 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -135,7 +135,7 @@ namespace osu.Game.Database where T : RealmObjectBase { // Subscribing to non-managed instances doesn't work. - // In this usage, the skin may be non-managed in tests. + // In this usage, the instance may be non-managed in tests. if (!(list is IRealmCollection realmCollection)) return null; @@ -162,7 +162,7 @@ namespace osu.Game.Database where T : RealmObjectBase { // Subscribing to non-managed instances doesn't work. - // In this usage, the skin may be non-managed in tests. + // In this usage, the instance may be non-managed in tests. if (!(list is IRealmCollection realmCollection)) return null; From c38537a51ab1a0e313ee0ed3b73d990c726ff214 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 22 Nov 2021 18:39:50 +0900 Subject: [PATCH 104/419] Initial implementation of MultiplayerPlaylist --- .../TestSceneMultiplayerPlaylist.cs | 255 ++++++++++++++++++ .../Online/Multiplayer/MultiplayerClient.cs | 9 +- .../OnlinePlay/DrawableRoomPlaylist.cs | 23 +- .../Multiplayer/MultiplayerRoomComposite.cs | 34 +++ 4 files changed, 297 insertions(+), 24 deletions(-) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs new file mode 100644 index 0000000000..ee5cb7f32c --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -0,0 +1,255 @@ +// 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.Diagnostics; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Tests.Resources; +using osuTK; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerPlaylist : MultiplayerTestScene + { + private BeatmapManager beatmaps; + private RulesetStore rulesets; + private BeatmapSetInfo importedSet; + private BeatmapInfo importedBeatmap; + + [Cached(typeof(UserLookupCache))] + private UserLookupCache lookupCache = new TestUserLookupCache(); + + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + } + + [SetUp] + public new void Setup() => Schedule(() => + { + Child = new MultiplayerPlaylist + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.4f, 0.8f) + }; + }); + + [SetUpSteps] + public new void SetUpSteps() + { + AddStep("import beatmap", () => + { + beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0); + }); + } + + [Test] + public void DoTest() + { + AddStep("change to round robin mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayersRoundRobin })); + AddStep("add playlist item for user 1", () => Client.AddPlaylistItem(new MultiplayerPlaylistItem + { + BeatmapID = importedBeatmap.OnlineID!.Value + })); + } + + public class MultiplayerPlaylist : MultiplayerRoomComposite + { + private QueueList queueList; + private DrawableRoomPlaylist historyList; + private bool firstPopulation = true; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + queueList = new QueueList(false, false, true) + { + RelativeSizeAxes = Axes.Both + }, + historyList = new DrawableRoomPlaylist(false, false, true) + { + RelativeSizeAxes = Axes.Both + } + } + } + }; + } + + protected override void OnRoomUpdated() + { + base.OnRoomUpdated(); + + if (Room == null) + return; + + if (!firstPopulation) return; + + foreach (var item in Room.Playlist) + PlaylistItemAdded(item); + + firstPopulation = false; + } + + protected override void PlaylistItemAdded(MultiplayerPlaylistItem item) + { + base.PlaylistItemAdded(item); + + if (item.Expired) + historyList.Items.Add(getPlaylistItem(item)); + else + queueList.Items.Add(getPlaylistItem(item)); + } + + protected override void PlaylistItemRemoved(long item) + { + base.PlaylistItemRemoved(item); + + queueList.Items.RemoveAll(i => i.ID == item); + } + + protected override void PlaylistItemChanged(MultiplayerPlaylistItem item) + { + base.PlaylistItemChanged(item); + + PlaylistItemRemoved(item.ID); + PlaylistItemAdded(item); + } + + private PlaylistItem getPlaylistItem(MultiplayerPlaylistItem item) => Playlist.Single(i => i.ID == item.ID); + } + + public class QueueList : DrawableRoomPlaylist + { + public readonly IBindable QueueMode = new Bindable(); + + public QueueList(bool allowEdit, bool allowSelection, bool reverse = false) + : base(allowEdit, allowSelection, reverse) + { + } + + protected override FillFlowContainer> CreateListFillFlowContainer() => new QueueFillFlowContainer + { + QueueMode = { BindTarget = QueueMode }, + Spacing = new Vector2(0, 2) + }; + + private class QueueFillFlowContainer : FillFlowContainer> + { + public readonly IBindable QueueMode = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + QueueMode.BindValueChanged(_ => InvalidateLayout()); + } + + public override IEnumerable FlowingChildren + { + get + { + switch (QueueMode.Value) + { + default: + return AliveInternalChildren.Where(d => d.IsPresent) + .OfType>() + .OrderBy(item => item.Model.ID); + + case Game.Online.Multiplayer.QueueMode.AllPlayersRoundRobin: + // TODO: THIS IS SO INEFFICIENT, can it be done any better? + + // Group all items by their owners. + var groups = AliveInternalChildren.Where(d => d.IsPresent) + .OfType>() + .GroupBy(item => item.Model.OwnerID) + .Select(g => g.ToArray()) + .ToArray(); + + if (groups.Length == 0) + return Enumerable.Empty(); + + // Find the initial picking order for the groups. The group with the smallest 'weight' picks first. + int[] groupWeights = new int[groups.Length]; + + for (int i = 0; i < groups.Length; i++) + { + groupWeights[i] = groups[i].Count(item => item.Model.Expired); + groups[i] = groups[i].Where(item => !item.Model.Expired).ToArray(); + } + + var result = new List(); + + // Simulate the playlist by picking in order from the smallest-weighted room each time until no longer able to. + while (true) + { + var candidateGroup = groups + // Map each group to an index. + .Select((items, index) => new { index, items }) + // Order groups by their weights. + .OrderBy(group => groupWeights[group.index]) + // Select the first group with remaining items (null is set from previous iterations). + .FirstOrDefault(group => group.items.Any(i => i != null)); + + // Iteration ends when all groups have been exhausted of items. + if (candidateGroup == null) + break; + + // Find the index of the first non-null (i.e. unused) item in the group. + int candidateItemIndex = 0; + RearrangeableListItem candidateItem = null; + + for (int i = 0; i < candidateGroup.items.Length; i++) + { + if (candidateGroup.items[i] != null) + { + candidateItemIndex = i; + candidateItem = candidateGroup.items[i]; + } + } + + // The item is guaranteed to not be expired, since we've previously removed all expired items. + Debug.Assert(candidateItem?.Model.Expired == false); + + // Add the item to the result set. + result.Add(candidateItem); + + // Update the group for the next iteration. + candidateGroup.items[candidateItemIndex] = null; + groupWeights[candidateGroup.index]++; + } + + return result; + } + } + } + } + } + } +} diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index df16fb3042..fb716c9814 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -33,11 +33,13 @@ namespace osu.Game.Online.Multiplayer public event Action? RoomUpdated; public event Action? UserJoined; - public event Action? UserLeft; - public event Action? UserKicked; + public event Action? ItemAdded; + public event Action? ItemRemoved; + public event Action? ItemChanged; + /// /// Invoked when the multiplayer server requests the current beatmap to be loaded into play. /// @@ -619,6 +621,7 @@ namespace osu.Game.Online.Multiplayer Room.Playlist.Add(item); APIRoom.Playlist.Add(playlistItem); + ItemAdded?.Invoke(item); RoomUpdated?.Invoke(); }); } @@ -638,6 +641,7 @@ namespace osu.Game.Online.Multiplayer Room.Playlist.Remove(Room.Playlist.Single(existing => existing.ID == playlistItemId)); APIRoom.Playlist.RemoveAll(existing => existing.ID == playlistItemId); + ItemRemoved?.Invoke(playlistItemId); RoomUpdated?.Invoke(); }); @@ -668,6 +672,7 @@ namespace osu.Game.Online.Multiplayer if (CurrentMatchPlayingItem.Value?.ID == playlistItem.ID) CurrentMatchPlayingItem.Value = playlistItem; + ItemChanged?.Invoke(item); RoomUpdated?.Invoke(); }); } diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index f5522cd25d..dc04d9a77b 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -1,9 +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.Collections.Specialized; -using System.Linq; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; @@ -25,8 +23,6 @@ namespace osu.Game.Screens.OnlinePlay { this.allowEdit = allowEdit; this.allowSelection = allowSelection; - - ((ReversibleFillFlowContainer)ListContainer).Reverse = reverse; } protected override void LoadComplete() @@ -51,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay d.ScrollbarVisible = false; }); - protected override FillFlowContainer> CreateListFillFlowContainer() => new ReversibleFillFlowContainer + protected override FillFlowContainer> CreateListFillFlowContainer() => new FillFlowContainer> { Spacing = new Vector2(0, 2) }; @@ -74,22 +70,5 @@ namespace osu.Game.Screens.OnlinePlay Items.Remove(item); } - - private class ReversibleFillFlowContainer : FillFlowContainer> - { - private bool reverse; - - public bool Reverse - { - get => reverse; - set - { - reverse = value; - Invalidate(); - } - } - - public override IEnumerable FlowingChildren => Reverse ? base.FlowingChildren.OrderBy(d => -GetLayoutPosition(d)) : base.FlowingChildren; - } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs index a380ddef25..f2cac708e4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs @@ -4,6 +4,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Multiplayer { @@ -23,6 +24,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Client.UserLeft += invokeUserLeft; Client.UserKicked += invokeUserKicked; Client.UserJoined += invokeUserJoined; + Client.ItemAdded += invokeItemAdded; + Client.ItemRemoved += invokeItemRemoved; + Client.ItemChanged += invokeItemChanged; OnRoomUpdated(); } @@ -31,6 +35,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void invokeUserJoined(MultiplayerRoomUser user) => Scheduler.AddOnce(UserJoined, user); private void invokeUserKicked(MultiplayerRoomUser user) => Scheduler.AddOnce(UserKicked, user); private void invokeUserLeft(MultiplayerRoomUser user) => Scheduler.AddOnce(UserLeft, user); + private void invokeItemAdded(MultiplayerPlaylistItem item) => Scheduler.AddOnce(PlaylistItemAdded, item); + private void invokeItemRemoved(long item) => Scheduler.AddOnce(PlaylistItemRemoved, item); + private void invokeItemChanged(MultiplayerPlaylistItem item) => Scheduler.AddOnce(PlaylistItemChanged, item); /// /// Invoked when a user has joined the room. @@ -56,6 +63,30 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { } + /// + /// Invoked when a playlist item is added to the room. + /// + /// The added playlist item. + protected virtual void PlaylistItemAdded(MultiplayerPlaylistItem item) + { + } + + /// + /// Invoked when a playlist item is removed from the room. + /// + /// The ID of the removed playlist item. + protected virtual void PlaylistItemRemoved(long item) + { + } + + /// + /// Invoked when a playlist item is changed in the room. + /// + /// The new playlist item, with an existing item's ID. + protected virtual void PlaylistItemChanged(MultiplayerPlaylistItem item) + { + } + /// /// Invoked when any change occurs to the multiplayer room. /// @@ -71,6 +102,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Client.UserLeft -= invokeUserLeft; Client.UserKicked -= invokeUserKicked; Client.UserJoined -= invokeUserJoined; + Client.ItemAdded -= invokeItemAdded; + Client.ItemRemoved -= invokeItemRemoved; + Client.ItemChanged -= invokeItemChanged; } base.Dispose(isDisposing); From 0cb35e8b1852d0c6f76a14db8382c84b25597ec9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 26 Nov 2021 19:24:49 +0900 Subject: [PATCH 105/419] Separate out QueueList --- .../TestSceneMultiplayerPlaylist.cs | 110 +--------------- .../Multiplayer/Match/Playlist/QueueList.cs | 121 ++++++++++++++++++ 2 files changed, 122 insertions(+), 109 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index ee5cb7f32c..3244e9420d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -1,13 +1,10 @@ // 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.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; @@ -19,6 +16,7 @@ using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; using osu.Game.Tests.Resources; using osuTK; @@ -145,111 +143,5 @@ namespace osu.Game.Tests.Visual.Multiplayer private PlaylistItem getPlaylistItem(MultiplayerPlaylistItem item) => Playlist.Single(i => i.ID == item.ID); } - - public class QueueList : DrawableRoomPlaylist - { - public readonly IBindable QueueMode = new Bindable(); - - public QueueList(bool allowEdit, bool allowSelection, bool reverse = false) - : base(allowEdit, allowSelection, reverse) - { - } - - protected override FillFlowContainer> CreateListFillFlowContainer() => new QueueFillFlowContainer - { - QueueMode = { BindTarget = QueueMode }, - Spacing = new Vector2(0, 2) - }; - - private class QueueFillFlowContainer : FillFlowContainer> - { - public readonly IBindable QueueMode = new Bindable(); - - protected override void LoadComplete() - { - base.LoadComplete(); - QueueMode.BindValueChanged(_ => InvalidateLayout()); - } - - public override IEnumerable FlowingChildren - { - get - { - switch (QueueMode.Value) - { - default: - return AliveInternalChildren.Where(d => d.IsPresent) - .OfType>() - .OrderBy(item => item.Model.ID); - - case Game.Online.Multiplayer.QueueMode.AllPlayersRoundRobin: - // TODO: THIS IS SO INEFFICIENT, can it be done any better? - - // Group all items by their owners. - var groups = AliveInternalChildren.Where(d => d.IsPresent) - .OfType>() - .GroupBy(item => item.Model.OwnerID) - .Select(g => g.ToArray()) - .ToArray(); - - if (groups.Length == 0) - return Enumerable.Empty(); - - // Find the initial picking order for the groups. The group with the smallest 'weight' picks first. - int[] groupWeights = new int[groups.Length]; - - for (int i = 0; i < groups.Length; i++) - { - groupWeights[i] = groups[i].Count(item => item.Model.Expired); - groups[i] = groups[i].Where(item => !item.Model.Expired).ToArray(); - } - - var result = new List(); - - // Simulate the playlist by picking in order from the smallest-weighted room each time until no longer able to. - while (true) - { - var candidateGroup = groups - // Map each group to an index. - .Select((items, index) => new { index, items }) - // Order groups by their weights. - .OrderBy(group => groupWeights[group.index]) - // Select the first group with remaining items (null is set from previous iterations). - .FirstOrDefault(group => group.items.Any(i => i != null)); - - // Iteration ends when all groups have been exhausted of items. - if (candidateGroup == null) - break; - - // Find the index of the first non-null (i.e. unused) item in the group. - int candidateItemIndex = 0; - RearrangeableListItem candidateItem = null; - - for (int i = 0; i < candidateGroup.items.Length; i++) - { - if (candidateGroup.items[i] != null) - { - candidateItemIndex = i; - candidateItem = candidateGroup.items[i]; - } - } - - // The item is guaranteed to not be expired, since we've previously removed all expired items. - Debug.Assert(candidateItem?.Model.Expired == false); - - // Add the item to the result set. - result.Add(candidateItem); - - // Update the group for the next iteration. - candidateGroup.items[candidateItemIndex] = null; - groupWeights[candidateGroup.index]++; - } - - return result; - } - } - } - } - } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs new file mode 100644 index 0000000000..5b691fdfda --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs @@ -0,0 +1,121 @@ +// 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.Diagnostics; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist +{ + public class QueueList : DrawableRoomPlaylist + { + public readonly IBindable QueueMode = new Bindable(); + + public QueueList(bool allowEdit, bool allowSelection, bool reverse = false) + : base(allowEdit, allowSelection, reverse) + { + } + + protected override FillFlowContainer> CreateListFillFlowContainer() => new QueueFillFlowContainer + { + QueueMode = { BindTarget = QueueMode }, + Spacing = new Vector2(0, 2) + }; + + private class QueueFillFlowContainer : FillFlowContainer> + { + public readonly IBindable QueueMode = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + QueueMode.BindValueChanged(_ => InvalidateLayout()); + } + + public override IEnumerable FlowingChildren + { + get + { + switch (QueueMode.Value) + { + default: + return AliveInternalChildren.Where(d => d.IsPresent) + .OfType>() + .OrderBy(item => item.Model.ID); + + case Game.Online.Multiplayer.QueueMode.AllPlayersRoundRobin: + // TODO: THIS IS SO INEFFICIENT, can it be done any better? + + // Group all items by their owners. + var groups = AliveInternalChildren.Where(d => d.IsPresent) + .OfType>() + .GroupBy(item => item.Model.OwnerID) + .Select(g => g.ToArray()) + .ToArray(); + + if (groups.Length == 0) + return Enumerable.Empty(); + + // Find the initial picking order for the groups. The group with the smallest 'weight' picks first. + int[] groupWeights = new int[groups.Length]; + + for (int i = 0; i < groups.Length; i++) + { + groupWeights[i] = groups[i].Count(item => item.Model.Expired); + groups[i] = groups[i].Where(item => !item.Model.Expired).ToArray(); + } + + var result = new List(); + + // Simulate the playlist by picking in order from the smallest-weighted room each time until no longer able to. + while (true) + { + var candidateGroup = groups + // Map each group to an index. + .Select((items, index) => new { index, items }) + // Order groups by their weights. + .OrderBy(group => groupWeights[group.index]) + // Select the first group with remaining items (null is set from previous iterations). + .FirstOrDefault(group => group.items.Any(i => i != null)); + + // Iteration ends when all groups have been exhausted of items. + if (candidateGroup == null) + break; + + // Find the index of the first non-null (i.e. unused) item in the group. + int candidateItemIndex = 0; + RearrangeableListItem candidateItem = null; + + for (int i = 0; i < candidateGroup.items.Length; i++) + { + if (candidateGroup.items[i] != null) + { + candidateItemIndex = i; + candidateItem = candidateGroup.items[i]; + } + } + + // The item is guaranteed to not be expired, since we've previously removed all expired items. + Debug.Assert(candidateItem?.Model.Expired == false); + + // Add the item to the result set. + result.Add(candidateItem); + + // Update the group for the next iteration. + candidateGroup.items[candidateItemIndex] = null; + groupWeights[candidateGroup.index]++; + } + + return result; + } + } + } + } + } +} From 68bb49fc1e795c86879ebcebaa6b599402531dd0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 26 Nov 2021 20:02:30 +0900 Subject: [PATCH 106/419] Add QueueList tests --- .../TestSceneMultiplayerQueueList.cs | 153 ++++++++++++++++++ .../Multiplayer/Match/Playlist/QueueList.cs | 2 +- 2 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs new file mode 100644 index 0000000000..b08b76a7ac --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -0,0 +1,153 @@ +// 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.Testing; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerQueueList : OsuTestScene + { + private QueueList list; + private int currentItemId; + + [SetUp] + public void Setup() => Schedule(() => + { + Child = list = new QueueList(false, false) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Width = 0.4f, + Height = 0.6f + }; + }); + + [Test] + public void TestItemsAddedToEndInHostOnlyMode() + { + changeQueueModeStep(QueueMode.HostOnly); + + // User 1. + + PlaylistItem item1 = addItemStep(1); + assertPositionStep(item1, 0); + + PlaylistItem item2 = addItemStep(1); + assertPositionStep(item2, 1); + + // User 2. + + PlaylistItem item3 = addItemStep(2); + assertPositionStep(item3, 2); + } + + [Test] + public void TestItemsAddedToEndInAllPlayersMode() + { + changeQueueModeStep(QueueMode.AllPlayers); + + // User 1. + + PlaylistItem item1 = addItemStep(1); + assertPositionStep(item1, 0); + + PlaylistItem item2 = addItemStep(1); + assertPositionStep(item2, 1); + + // User 2. + + PlaylistItem item3 = addItemStep(2); + assertPositionStep(item3, 2); + } + + [Test] + public void TestItemsInsertedInCorrectPositionInRoundRobinMode() + { + changeQueueModeStep(QueueMode.AllPlayersRoundRobin); + + // User 1. + + PlaylistItem item1 = addItemStep(1); + assertPositionStep(item1, 0); + + PlaylistItem item2 = addItemStep(1); + assertPositionStep(item2, 1); + + // User 2. + + PlaylistItem item3 = addItemStep(2); + assertPositionStep(item3, 1); + assertPositionStep(item2, 2); + + PlaylistItem item4 = addItemStep(2); + assertPositionStep(item4, 3); + + PlaylistItem item5 = addItemStep(2); + assertPositionStep(item5, 4); + + // User 1. + + PlaylistItem item6 = addItemStep(1); + assertPositionStep(item6, 4); + assertPositionStep(item5, 5); + + // User 3. + + PlaylistItem item7 = addItemStep(3); + assertPositionStep(item7, 2); + + PlaylistItem item8 = addItemStep(3); + assertPositionStep(item8, 5); + + PlaylistItem item9 = addItemStep(3); + assertPositionStep(item9, 8); + } + + /// + /// Adds a step to create a new playlist item. + /// + /// The item owner. + /// The playlist item's ID. + private PlaylistItem addItemStep(int ownerId) + { + var item = new PlaylistItem + { + ID = ++currentItemId, + OwnerID = ownerId, + Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo, false).BeatmapInfo } + }; + + AddStep($"add {{ item: {item.ID}, user: {ownerId} }}", () => list.Items.Add(item)); + + return item; + } + + /// + /// Asserts the position of a given playlist item in the visual layout of the list. + /// + /// The playlist item. + /// The index at which the item should appear visually. The item with index 0 is at the top of the list. + private void assertPositionStep(PlaylistItem item, int visualIndex) + { + AddUntilStep($"item {item.ID} has pos = {visualIndex}", () => + { + return this.ChildrenOfType() + .OrderBy(drawable => drawable.Position.Y) + .TakeWhile(drawable => drawable.Item.ID != item.ID) + .Count() == visualIndex; + }); + } + + private void changeQueueModeStep(QueueMode newMode) => AddStep($"change queue mode to {newMode}", () => list.QueueMode.Value = newMode); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs index 5b691fdfda..971196865b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist { public class QueueList : DrawableRoomPlaylist { - public readonly IBindable QueueMode = new Bindable(); + public readonly Bindable QueueMode = new Bindable(); public QueueList(bool allowEdit, bool allowSelection, bool reverse = false) : base(allowEdit, allowSelection, reverse) From 9806c75743387baa015630b611f4355451fa2ae2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 26 Nov 2021 21:13:21 +0900 Subject: [PATCH 107/419] Implement better round robin algorithm --- .../Multiplayer/Match/Playlist/QueueList.cs | 77 ++++++------------- 1 file changed, 23 insertions(+), 54 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs index 971196865b..6d4ea5c7fa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs @@ -1,8 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -50,66 +50,35 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist .OrderBy(item => item.Model.ID); case Game.Online.Multiplayer.QueueMode.AllPlayersRoundRobin: - // TODO: THIS IS SO INEFFICIENT, can it be done any better? + RearrangeableListItem[] allItems = AliveInternalChildren + .Where(d => d.IsPresent) + .OfType>() + .OrderBy(item => item.Model.ID) + .ToArray(); - // Group all items by their owners. - var groups = AliveInternalChildren.Where(d => d.IsPresent) - .OfType>() - .GroupBy(item => item.Model.OwnerID) - .Select(g => g.ToArray()) - .ToArray(); - - if (groups.Length == 0) + int totalCount = allItems.Length; + if (totalCount == 0) return Enumerable.Empty(); - // Find the initial picking order for the groups. The group with the smallest 'weight' picks first. - int[] groupWeights = new int[groups.Length]; + Dictionary perUserCounts = allItems + .Select(item => item.Model.OwnerID) + .Distinct() + .ToDictionary(u => u, _ => 0); - for (int i = 0; i < groups.Length; i++) + List result = new List(); + + while (totalCount-- > 0) { - groupWeights[i] = groups[i].Count(item => item.Model.Expired); - groups[i] = groups[i].Where(item => !item.Model.Expired).ToArray(); - } + var candidateItem = allItems + .Where(item => item != null) + .OrderBy(item => perUserCounts[item.Model.OwnerID]) + .First(); - var result = new List(); + int itemIndex = Array.IndexOf(allItems, candidateItem); - // Simulate the playlist by picking in order from the smallest-weighted room each time until no longer able to. - while (true) - { - var candidateGroup = groups - // Map each group to an index. - .Select((items, index) => new { index, items }) - // Order groups by their weights. - .OrderBy(group => groupWeights[group.index]) - // Select the first group with remaining items (null is set from previous iterations). - .FirstOrDefault(group => group.items.Any(i => i != null)); - - // Iteration ends when all groups have been exhausted of items. - if (candidateGroup == null) - break; - - // Find the index of the first non-null (i.e. unused) item in the group. - int candidateItemIndex = 0; - RearrangeableListItem candidateItem = null; - - for (int i = 0; i < candidateGroup.items.Length; i++) - { - if (candidateGroup.items[i] != null) - { - candidateItemIndex = i; - candidateItem = candidateGroup.items[i]; - } - } - - // The item is guaranteed to not be expired, since we've previously removed all expired items. - Debug.Assert(candidateItem?.Model.Expired == false); - - // Add the item to the result set. - result.Add(candidateItem); - - // Update the group for the next iteration. - candidateGroup.items[candidateItemIndex] = null; - groupWeights[candidateGroup.index]++; + result.Add(allItems[itemIndex]); + perUserCounts[candidateItem.Model.OwnerID]++; + allItems[itemIndex] = null; } return result; From 48a181af1f002df2ead3c7c1764497acd3c26a5f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 30 Nov 2021 19:09:45 +0900 Subject: [PATCH 108/419] Fix test button display --- .../Visual/Multiplayer/TestSceneMultiplayerQueueList.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index b08b76a7ac..f52683df8e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -32,6 +32,13 @@ namespace osu.Game.Tests.Visual.Multiplayer }; }); + [SetUpSteps] + public void SetUpSteps() + { + // Not scheduled since this should affect buttons added by the current test. + currentItemId = 0; + } + [Test] public void TestItemsAddedToEndInHostOnlyMode() { From 7e800659aa1fec2acac5b70e48f30efba4bbf260 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 30 Nov 2021 19:09:52 +0900 Subject: [PATCH 109/419] Fix incorrect assertion --- .../Visual/Multiplayer/TestSceneMultiplayerQueueList.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index f52683df8e..c88b8bf4d5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -104,9 +104,10 @@ namespace osu.Game.Tests.Visual.Multiplayer // User 1. + // This item is added to the end rather than injected between item4 and item5, since both users have an equal number + // of added items at this point and this user was the last of the two to add an item. PlaylistItem item6 = addItemStep(1); - assertPositionStep(item6, 4); - assertPositionStep(item5, 5); + assertPositionStep(item6, 5); // User 3. From 6b198ce11260e8a1884de26d27b67f0e9b21f2a3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 30 Nov 2021 20:12:18 +0900 Subject: [PATCH 110/419] Document simulation --- .../OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs index 6d4ea5c7fa..a783e66ee6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs @@ -67,6 +67,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist List result = new List(); + // Run a simulation... + // In every iteration, pick the first available item from the user with the lowest number of items in the queue to add to the result set. + // If multiple users have the same number of items in the queue, then the item with the lowest ID is chosen. + // Todo: This could probably be more efficient, likely at the cost of increased complexity. while (totalCount-- > 0) { var candidateItem = allItems From 89d22824c354dedbc19d8312ccb6f7fa5402a98c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 30 Nov 2021 20:54:20 +0900 Subject: [PATCH 111/419] Fix incorrect comment --- .../Visual/Multiplayer/TestSceneMultiplayerQueueList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index c88b8bf4d5..d6b53819c9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUpSteps] public void SetUpSteps() { - // Not scheduled since this should affect buttons added by the current test. + // Not inside a step since this is used to affect steps added by the current test. currentItemId = 0; } From 01108016a7f45c7e9d47a1cc77d46128846ffd9f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 30 Nov 2021 21:13:38 +0900 Subject: [PATCH 112/419] Add reorder test --- .../TestSceneMultiplayerQueueList.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index d6b53819c9..83c611e325 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.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 NUnit.Framework; using osu.Framework.Graphics; @@ -121,6 +122,28 @@ namespace osu.Game.Tests.Visual.Multiplayer assertPositionStep(item9, 8); } + [Test] + public void TestItemsReorderedWhenQueueModeChanged() + { + changeQueueModeStep(QueueMode.AllPlayers); + + var items = new List(); + + for (int i = 0; i < 8; i++) + items.Add(addItemStep(i <= 3 ? 1 : 2)); + + for (int i = 0; i < 8; i++) + assertPositionStep(items[i], i); + + changeQueueModeStep(QueueMode.AllPlayersRoundRobin); + + for (int i = 0; i < 4; i++) + { + assertPositionStep(items[i], i * 2); // Items by user 1. + assertPositionStep(items[i + 4], i * 2 + 1); // Items by user 2. + } + } + /// /// Adds a step to create a new playlist item. /// From bfd2dc28c896e109926f8111bb7891e7ccf1234b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 16:56:32 +0900 Subject: [PATCH 113/419] Rename QueueList -> MultiplayerQueueList --- .../Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs | 8 ++++---- .../Visual/Multiplayer/TestSceneMultiplayerQueueList.cs | 4 ++-- .../Playlist/{QueueList.cs => MultiplayerQueueList.cs} | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) rename osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/{QueueList.cs => MultiplayerQueueList.cs} (96%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 3244e9420d..977a6f0115 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public class MultiplayerPlaylist : MultiplayerRoomComposite { - private QueueList queueList; + private MultiplayerQueueList multiplayerQueueList; private DrawableRoomPlaylist historyList; private bool firstPopulation = true; @@ -88,7 +88,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new Drawable[] { - queueList = new QueueList(false, false, true) + multiplayerQueueList = new MultiplayerQueueList(false, false, true) { RelativeSizeAxes = Axes.Both }, @@ -123,14 +123,14 @@ namespace osu.Game.Tests.Visual.Multiplayer if (item.Expired) historyList.Items.Add(getPlaylistItem(item)); else - queueList.Items.Add(getPlaylistItem(item)); + multiplayerQueueList.Items.Add(getPlaylistItem(item)); } protected override void PlaylistItemRemoved(long item) { base.PlaylistItemRemoved(item); - queueList.Items.RemoveAll(i => i.ID == item); + multiplayerQueueList.Items.RemoveAll(i => i.ID == item); } protected override void PlaylistItemChanged(MultiplayerPlaylistItem item) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 83c611e325..2186d2e594 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -17,13 +17,13 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayerQueueList : OsuTestScene { - private QueueList list; + private MultiplayerQueueList list; private int currentItemId; [SetUp] public void Setup() => Schedule(() => { - Child = list = new QueueList(false, false) + Child = list = new MultiplayerQueueList(false, false) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs similarity index 96% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index a783e66ee6..de3ad7b824 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/QueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -13,11 +13,11 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist { - public class QueueList : DrawableRoomPlaylist + public class MultiplayerQueueList : DrawableRoomPlaylist { public readonly Bindable QueueMode = new Bindable(); - public QueueList(bool allowEdit, bool allowSelection, bool reverse = false) + public MultiplayerQueueList(bool allowEdit, bool allowSelection, bool reverse = false) : base(allowEdit, allowSelection, reverse) { } From e0ca1af9b896565bf41f2bb8d75d4ae0413b59c4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 16:58:48 +0900 Subject: [PATCH 114/419] Remove ctor params --- .../Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs | 2 +- .../Visual/Multiplayer/TestSceneMultiplayerQueueList.cs | 2 +- .../Multiplayer/Match/Playlist/MultiplayerQueueList.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 977a6f0115..b9f147e44a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -88,7 +88,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new Drawable[] { - multiplayerQueueList = new MultiplayerQueueList(false, false, true) + multiplayerQueueList = new MultiplayerQueueList { RelativeSizeAxes = Axes.Both }, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 2186d2e594..2e24d54b59 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUp] public void Setup() => Schedule(() => { - Child = list = new MultiplayerQueueList(false, false) + Child = list = new MultiplayerQueueList { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index de3ad7b824..802603d7e1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -17,8 +17,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist { public readonly Bindable QueueMode = new Bindable(); - public MultiplayerQueueList(bool allowEdit, bool allowSelection, bool reverse = false) - : base(allowEdit, allowSelection, reverse) + public MultiplayerQueueList() + : base(false, false, false, true) { } From fc8c8685b87dd958a8cdeb0d1abba6157a120926 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 18:44:46 +0900 Subject: [PATCH 115/419] Add playlist queue tests --- .../TestSceneMultiplayerPlaylist.cs | 145 +++++++++++++++--- .../Match/Playlist/MultiplayerHistoryList.cs | 30 ++++ .../Multiplayer/TestMultiplayerClient.cs | 4 +- 3 files changed, 158 insertions(+), 21 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index b9f147e44a..136625682a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; @@ -29,9 +28,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapSetInfo importedSet; private BeatmapInfo importedBeatmap; - [Cached(typeof(UserLookupCache))] - private UserLookupCache lookupCache = new TestUserLookupCache(); - [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { @@ -60,22 +56,132 @@ namespace osu.Game.Tests.Visual.Multiplayer importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0); }); + + AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); } [Test] - public void DoTest() + public void TestNonExpiredItemsAddedToQueueList() { - AddStep("change to round robin mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayersRoundRobin })); - AddStep("add playlist item for user 1", () => Client.AddPlaylistItem(new MultiplayerPlaylistItem - { - BeatmapID = importedBeatmap.OnlineID!.Value - })); + assertItemInQueueListStep(1, 0); + + addItemStep(); + assertItemInQueueListStep(2, 1); + + addItemStep(); + assertItemInQueueListStep(3, 2); + } + + [Test] + public void TestExpiredItemsAddedToHistoryList() + { + assertItemInQueueListStep(1, 0); + + addItemStep(true); + assertItemInHistoryListStep(2, 0); + + addItemStep(true); + assertItemInHistoryListStep(3, 0); + assertItemInHistoryListStep(2, 1); + + // Initial item is still in the queue. + assertItemInQueueListStep(1, 0); + } + + [Test] + public void TestExpiredItemsMoveToQueueList() + { + addItemStep(); + addItemStep(); + + AddStep("finish current item", () => Client.FinishCurrentItem()); + + assertItemInHistoryListStep(1, 0); + assertItemInQueueListStep(2, 0); + assertItemInQueueListStep(3, 1); + + AddStep("finish current item", () => Client.FinishCurrentItem()); + + assertItemInHistoryListStep(2, 0); + assertItemInHistoryListStep(1, 1); + assertItemInQueueListStep(3, 0); + + AddStep("finish current item", () => Client.FinishCurrentItem()); + + assertItemInHistoryListStep(3, 0); + assertItemInHistoryListStep(2, 1); + assertItemInHistoryListStep(1, 2); + } + + [Test] + public void TestJoinRoomWithMixedItemsAddedInCorrectLists() + { + AddStep("leave room", () => RoomManager.PartRoom()); + AddUntilStep("wait for room part", () => Client.Room == null); + } + + /// + /// Adds a step to create a new playlist item. + /// + private void addItemStep(bool expired = false) => AddStep("add item", () => Client.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem + { + Beatmap = { Value = importedBeatmap }, + BeatmapID = importedBeatmap.OnlineID ?? -1, + Expired = expired + }))); + + /// + /// Asserts the position of a given playlist item in the queue list. + /// + /// The item id. + /// The index at which the item should appear visually. The item with index 0 is at the top of the list. + private void assertItemInQueueListStep(int playlistItemId, int visualIndex) => AddUntilStep($"{playlistItemId} in queue at pos = {visualIndex}", () => + { + return !inHistoryList(playlistItemId) + && this.ChildrenOfType() + .Single() + .ChildrenOfType() + .OrderBy(drawable => drawable.Position.Y) + .TakeWhile(drawable => drawable.Item.ID != playlistItemId) + .Count() == visualIndex; + }); + + /// + /// Asserts the position of a given playlist item in the history list. + /// + /// The item id. + /// The index at which the item should appear visually. The item with index 0 is at the top of the list. + private void assertItemInHistoryListStep(int playlistItemId, int visualIndex) => AddUntilStep($"{playlistItemId} in history at pos = {visualIndex}", () => + { + return !inQueueList(playlistItemId) + && this.ChildrenOfType() + .Single() + .ChildrenOfType() + .OrderBy(drawable => drawable.Position.Y) + .TakeWhile(drawable => drawable.Item.ID != playlistItemId) + .Count() == visualIndex; + }); + + private bool inQueueList(int playlistItemId) + { + return this.ChildrenOfType() + .Single() + .ChildrenOfType() + .Any(i => i.Item.ID == playlistItemId); + } + + private bool inHistoryList(int playlistItemId) + { + return this.ChildrenOfType() + .Single() + .ChildrenOfType() + .Any(i => i.Item.ID == playlistItemId); } public class MultiplayerPlaylist : MultiplayerRoomComposite { - private MultiplayerQueueList multiplayerQueueList; - private DrawableRoomPlaylist historyList; + private MultiplayerQueueList queueList; + private MultiplayerHistoryList historyList; private bool firstPopulation = true; [BackgroundDependencyLoader] @@ -88,11 +194,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { new Drawable[] { - multiplayerQueueList = new MultiplayerQueueList + queueList = new MultiplayerQueueList { RelativeSizeAxes = Axes.Both }, - historyList = new DrawableRoomPlaylist(false, false, true) + historyList = new MultiplayerHistoryList { RelativeSizeAxes = Axes.Both } @@ -120,17 +226,20 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.PlaylistItemAdded(item); + var apiItem = Playlist.Single(i => i.ID == item.ID); + if (item.Expired) - historyList.Items.Add(getPlaylistItem(item)); + historyList.Items.Add(apiItem); else - multiplayerQueueList.Items.Add(getPlaylistItem(item)); + queueList.Items.Add(apiItem); } protected override void PlaylistItemRemoved(long item) { base.PlaylistItemRemoved(item); - multiplayerQueueList.Items.RemoveAll(i => i.ID == item); + queueList.Items.RemoveAll(i => i.ID == item); + historyList.Items.RemoveAll(i => i.ID == item); } protected override void PlaylistItemChanged(MultiplayerPlaylistItem item) @@ -140,8 +249,6 @@ namespace osu.Game.Tests.Visual.Multiplayer PlaylistItemRemoved(item.ID); PlaylistItemAdded(item); } - - private PlaylistItem getPlaylistItem(MultiplayerPlaylistItem item) => Playlist.Single(i => i.ID == item.ID); } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs new file mode 100644 index 0000000000..76bb364f3a --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.Rooms; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist +{ + public class MultiplayerHistoryList : DrawableRoomPlaylist + { + public MultiplayerHistoryList() + : base(false, false, false, true) + { + } + + protected override FillFlowContainer> CreateListFillFlowContainer() => new HistoryFillFlowContainer + { + Spacing = new Vector2(0, 2) + }; + + private class HistoryFillFlowContainer : FillFlowContainer> + { + public override IEnumerable FlowingChildren => base.FlowingChildren.Reverse(); + } + } +} diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 2d77e17513..b746f7e667 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ((IMultiplayerClient)this).ResultsReady(); - finishCurrentItem().Wait(); + FinishCurrentItem().Wait(); } break; @@ -390,7 +390,7 @@ namespace osu.Game.Tests.Visual.Multiplayer await updateCurrentItem(Room).ConfigureAwait(false); } - private async Task finishCurrentItem() + public async Task FinishCurrentItem() { Debug.Assert(Room != null); Debug.Assert(APIRoom != null); From a4cd22d5a97295bb2de4b53026ae33afd1a40e56 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 18:57:29 +0900 Subject: [PATCH 116/419] Clear lists on room leave --- .../Multiplayer/TestSceneMultiplayerPlaylist.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 136625682a..74284e21f1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -113,6 +113,19 @@ namespace osu.Game.Tests.Visual.Multiplayer assertItemInHistoryListStep(1, 2); } + [Test] + public void TestListsClearedWhenRoomLeft() + { + addItemStep(); + AddStep("finish current item", () => Client.FinishCurrentItem()); + + AddStep("leave room", () => RoomManager.PartRoom()); + AddUntilStep("wait for room part", () => Client.Room == null); + + AddUntilStep("item 0 not in lists", () => !inHistoryList(0) && !inQueueList(0)); + AddUntilStep("item 1 not in lists", () => !inHistoryList(0) && !inQueueList(0)); + } + [Test] public void TestJoinRoomWithMixedItemsAddedInCorrectLists() { @@ -212,7 +225,11 @@ namespace osu.Game.Tests.Visual.Multiplayer base.OnRoomUpdated(); if (Room == null) + { + historyList.Items.Clear(); + queueList.Items.Clear(); return; + } if (!firstPopulation) return; From 0b3cc47a51645d67892ac1182a4a9d3b8141eb6b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 19:02:17 +0900 Subject: [PATCH 117/419] Fix list not repopulating on new room --- .../TestSceneMultiplayerPlaylist.cs | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 74284e21f1..df34e3b6af 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; +using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; using osuTK; @@ -131,6 +132,33 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("leave room", () => RoomManager.PartRoom()); AddUntilStep("wait for room part", () => Client.Room == null); + + AddStep("join room with items", () => + { + RoomManager.CreateRoom(new Room + { + Name = { Value = "test name" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, + Ruleset = { Value = Ruleset.Value } + }, + new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, + Ruleset = { Value = Ruleset.Value }, + Expired = true + } + } + }); + }); + + AddUntilStep("wait for room join", () => RoomJoined); + + assertItemInQueueListStep(1, 0); + assertItemInHistoryListStep(2, 0); } /// @@ -228,15 +256,17 @@ namespace osu.Game.Tests.Visual.Multiplayer { historyList.Items.Clear(); queueList.Items.Clear(); + firstPopulation = true; return; } - if (!firstPopulation) return; + if (firstPopulation) + { + foreach (var item in Room.Playlist) + PlaylistItemAdded(item); - foreach (var item in Room.Playlist) - PlaylistItemAdded(item); - - firstPopulation = false; + firstPopulation = false; + } } protected override void PlaylistItemAdded(MultiplayerPlaylistItem item) From 11c137cf83523a76904a6f2eb539f80b07a30874 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 19:22:21 +0900 Subject: [PATCH 118/419] Ignore test --- .../Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index df34e3b6af..3117db8900 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -127,6 +127,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("item 1 not in lists", () => !inHistoryList(0) && !inQueueList(0)); } + [Ignore("Expired items are initially removed from the room.")] [Test] public void TestJoinRoomWithMixedItemsAddedInCorrectLists() { From 95050d6597e559879b6103899979e7529f00d936 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 19:23:35 +0900 Subject: [PATCH 119/419] Extract class to file --- .../TestSceneMultiplayerPlaylist.cs | 81 ----------------- .../Match/Playlist/MultiplayerPlaylist.cs | 90 +++++++++++++++++++ 2 files changed, 90 insertions(+), 81 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 3117db8900..b21dbbd462 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -6,7 +6,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -14,7 +13,6 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay; -using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; @@ -219,84 +217,5 @@ namespace osu.Game.Tests.Visual.Multiplayer .ChildrenOfType() .Any(i => i.Item.ID == playlistItemId); } - - public class MultiplayerPlaylist : MultiplayerRoomComposite - { - private MultiplayerQueueList queueList; - private MultiplayerHistoryList historyList; - private bool firstPopulation = true; - - [BackgroundDependencyLoader] - private void load() - { - InternalChild = new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] - { - queueList = new MultiplayerQueueList - { - RelativeSizeAxes = Axes.Both - }, - historyList = new MultiplayerHistoryList - { - RelativeSizeAxes = Axes.Both - } - } - } - }; - } - - protected override void OnRoomUpdated() - { - base.OnRoomUpdated(); - - if (Room == null) - { - historyList.Items.Clear(); - queueList.Items.Clear(); - firstPopulation = true; - return; - } - - if (firstPopulation) - { - foreach (var item in Room.Playlist) - PlaylistItemAdded(item); - - firstPopulation = false; - } - } - - protected override void PlaylistItemAdded(MultiplayerPlaylistItem item) - { - base.PlaylistItemAdded(item); - - var apiItem = Playlist.Single(i => i.ID == item.ID); - - if (item.Expired) - historyList.Items.Add(apiItem); - else - queueList.Items.Add(apiItem); - } - - protected override void PlaylistItemRemoved(long item) - { - base.PlaylistItemRemoved(item); - - queueList.Items.RemoveAll(i => i.ID == item); - historyList.Items.RemoveAll(i => i.ID == item); - } - - protected override void PlaylistItemChanged(MultiplayerPlaylistItem item) - { - base.PlaylistItemChanged(item); - - PlaylistItemRemoved(item.ID); - PlaylistItemAdded(item); - } - } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs new file mode 100644 index 0000000000..f853333b08 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -0,0 +1,90 @@ +// 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.Online.Rooms; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist +{ + public class MultiplayerPlaylist : MultiplayerRoomComposite + { + private MultiplayerQueueList queueList; + private MultiplayerHistoryList historyList; + private bool firstPopulation = true; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + queueList = new MultiplayerQueueList + { + RelativeSizeAxes = Axes.Both + }, + historyList = new MultiplayerHistoryList + { + RelativeSizeAxes = Axes.Both + } + } + } + }; + } + + protected override void OnRoomUpdated() + { + base.OnRoomUpdated(); + + if (Room == null) + { + historyList.Items.Clear(); + queueList.Items.Clear(); + firstPopulation = true; + return; + } + + if (firstPopulation) + { + foreach (var item in Room.Playlist) + PlaylistItemAdded(item); + + firstPopulation = false; + } + } + + protected override void PlaylistItemAdded(MultiplayerPlaylistItem item) + { + base.PlaylistItemAdded(item); + + var apiItem = Playlist.Single(i => i.ID == item.ID); + + if (item.Expired) + historyList.Items.Add(apiItem); + else + queueList.Items.Add(apiItem); + } + + protected override void PlaylistItemRemoved(long item) + { + base.PlaylistItemRemoved(item); + + queueList.Items.RemoveAll(i => i.ID == item); + historyList.Items.RemoveAll(i => i.ID == item); + } + + protected override void PlaylistItemChanged(MultiplayerPlaylistItem item) + { + base.PlaylistItemChanged(item); + + PlaylistItemRemoved(item.ID); + PlaylistItemAdded(item); + } + } +} From 7847ce6253e5c3bc8ba43b838852273d65f8bc68 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 19:44:29 +0900 Subject: [PATCH 120/419] Redesign with tab control --- .../Match/Playlist/MultiplayerPlaylist.cs | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index f853333b08..a44e086814 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -3,8 +3,11 @@ using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist @@ -18,24 +21,42 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist [BackgroundDependencyLoader] private void load() { - InternalChild = new GridContainer + TabControl displayModeTabControl; + + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Content = new[] + displayModeTabControl = new OsuTabControl { - new Drawable[] + RelativeSizeAxes = Axes.X, + Height = 25 + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 27 }, + Masking = true, + Children = new Drawable[] { queueList = new MultiplayerQueueList { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, }, historyList = new MultiplayerHistoryList { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + Alpha = 0, } } } }; + + displayModeTabControl.Current.BindValueChanged(onDisplayModeChanged, true); + } + + private void onDisplayModeChanged(ValueChangedEvent mode) + { + historyList.FadeTo(mode.NewValue == DisplayMode.History ? 1 : 0, 100); + queueList.FadeTo(mode.NewValue == DisplayMode.Queue ? 1 : 0, 100); } protected override void OnRoomUpdated() @@ -86,5 +107,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist PlaylistItemRemoved(item.ID); PlaylistItemAdded(item); } + + private enum DisplayMode + { + Queue, + History, + } } } From 1152c4e8e9bb91b6dc5209645cc9f2da69ac5eb0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 19:54:39 +0900 Subject: [PATCH 121/419] Fix tests --- .../TestSceneMultiplayerPlaylist.cs | 57 +++++++++++-------- .../Match/Playlist/MultiplayerPlaylist.cs | 31 +++++----- .../MultiplayerPlaylistDisplayMode.cs | 11 ++++ 3 files changed, 60 insertions(+), 39 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistDisplayMode.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index b21dbbd462..ae885685f7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -22,6 +22,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayerPlaylist : MultiplayerTestScene { + private MultiplayerPlaylist list; private BeatmapManager beatmaps; private RulesetStore rulesets; private BeatmapSetInfo importedSet; @@ -37,7 +38,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUp] public new void Setup() => Schedule(() => { - Child = new MultiplayerPlaylist + Child = list = new MultiplayerPlaylist { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -175,47 +176,57 @@ namespace osu.Game.Tests.Visual.Multiplayer /// /// The item id. /// The index at which the item should appear visually. The item with index 0 is at the top of the list. - private void assertItemInQueueListStep(int playlistItemId, int visualIndex) => AddUntilStep($"{playlistItemId} in queue at pos = {visualIndex}", () => + private void assertItemInQueueListStep(int playlistItemId, int visualIndex) { - return !inHistoryList(playlistItemId) - && this.ChildrenOfType() - .Single() - .ChildrenOfType() - .OrderBy(drawable => drawable.Position.Y) - .TakeWhile(drawable => drawable.Item.ID != playlistItemId) - .Count() == visualIndex; - }); + changeDisplayModeStep(MultiplayerPlaylistDisplayMode.Queue); + + AddUntilStep($"{playlistItemId} in queue at pos = {visualIndex}", () => + { + return !inHistoryList(playlistItemId) + && this.ChildrenOfType() + .Single() + .ChildrenOfType() + .OrderBy(drawable => drawable.Position.Y) + .TakeWhile(drawable => drawable.Item.ID != playlistItemId) + .Count() == visualIndex; + }); + } /// /// Asserts the position of a given playlist item in the history list. /// /// The item id. /// The index at which the item should appear visually. The item with index 0 is at the top of the list. - private void assertItemInHistoryListStep(int playlistItemId, int visualIndex) => AddUntilStep($"{playlistItemId} in history at pos = {visualIndex}", () => + private void assertItemInHistoryListStep(int playlistItemId, int visualIndex) { - return !inQueueList(playlistItemId) - && this.ChildrenOfType() - .Single() - .ChildrenOfType() - .OrderBy(drawable => drawable.Position.Y) - .TakeWhile(drawable => drawable.Item.ID != playlistItemId) - .Count() == visualIndex; - }); + changeDisplayModeStep(MultiplayerPlaylistDisplayMode.History); + + AddUntilStep($"{playlistItemId} in history at pos = {visualIndex}", () => + { + return !inQueueList(playlistItemId) + && this.ChildrenOfType() + .Single() + .ChildrenOfType() + .OrderBy(drawable => drawable.Position.Y) + .TakeWhile(drawable => drawable.Item.ID != playlistItemId) + .Count() == visualIndex; + }); + } + + private void changeDisplayModeStep(MultiplayerPlaylistDisplayMode mode) => AddStep($"change list to {mode}", () => list.DisplayMode.Value = mode); private bool inQueueList(int playlistItemId) { return this.ChildrenOfType() .Single() - .ChildrenOfType() - .Any(i => i.Item.ID == playlistItemId); + .Items.Any(i => i.ID == playlistItemId); } private bool inHistoryList(int playlistItemId) { return this.ChildrenOfType() .Single() - .ChildrenOfType() - .Any(i => i.Item.ID == playlistItemId); + .Items.Any(i => i.ID == playlistItemId); } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index a44e086814..aebe70f95a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -6,7 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Rooms; @@ -14,6 +13,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist { public class MultiplayerPlaylist : MultiplayerRoomComposite { + public readonly Bindable DisplayMode = new Bindable(); + private MultiplayerQueueList queueList; private MultiplayerHistoryList historyList; private bool firstPopulation = true; @@ -21,14 +22,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist [BackgroundDependencyLoader] private void load() { - TabControl displayModeTabControl; - InternalChildren = new Drawable[] { - displayModeTabControl = new OsuTabControl + new OsuTabControl { RelativeSizeAxes = Axes.X, - Height = 25 + Height = 25, + Current = { BindTarget = DisplayMode } }, new Container { @@ -49,14 +49,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist } } }; - - displayModeTabControl.Current.BindValueChanged(onDisplayModeChanged, true); } - private void onDisplayModeChanged(ValueChangedEvent mode) + protected override void LoadComplete() { - historyList.FadeTo(mode.NewValue == DisplayMode.History ? 1 : 0, 100); - queueList.FadeTo(mode.NewValue == DisplayMode.Queue ? 1 : 0, 100); + base.LoadComplete(); + + DisplayMode.BindValueChanged(onDisplayModeChanged, true); + } + + private void onDisplayModeChanged(ValueChangedEvent mode) + { + historyList.FadeTo(mode.NewValue == MultiplayerPlaylistDisplayMode.History ? 1 : 0, 100); + queueList.FadeTo(mode.NewValue == MultiplayerPlaylistDisplayMode.Queue ? 1 : 0, 100); } protected override void OnRoomUpdated() @@ -107,11 +112,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist PlaylistItemRemoved(item.ID); PlaylistItemAdded(item); } - - private enum DisplayMode - { - Queue, - History, - } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistDisplayMode.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistDisplayMode.cs new file mode 100644 index 0000000000..af1fac1c79 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistDisplayMode.cs @@ -0,0 +1,11 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist +{ + public enum MultiplayerPlaylistDisplayMode + { + Queue, + History, + } +} From c3dfe10a8a637d472500dc67e9d4c1cd5b7bc17d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 19:54:48 +0900 Subject: [PATCH 122/419] Add new list to match subscreen --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 077e9cef93..16017a1f0e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -26,6 +26,7 @@ using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play; @@ -56,8 +57,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [CanBeNull] private IDisposable readyClickOperation; - private DrawableRoomPlaylist playlist; - public MultiplayerMatchSubScreen(Room room) : base(room) { @@ -74,9 +73,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true); UserMods.BindValueChanged(onUserModsChanged); - playlist.Items.BindTo(Room.Playlist); - playlist.SelectedItem.BindTo(SelectedItem); - client.LoadRequested += onLoadRequested; client.RoomUpdated += onRoomUpdated; @@ -153,10 +149,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer null, new Drawable[] { - playlist = new DrawableRoomPlaylist(false, false, true, true) + new MultiplayerPlaylist { - RelativeSizeAxes = Axes.Both, - }, + RelativeSizeAxes = Axes.Both + } }, new[] { From d70355237df75dd4b8e374fed92e4f3ab7b7bd7a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 19:55:53 +0900 Subject: [PATCH 123/419] Fix selected item not bound --- .../Multiplayer/Match/Playlist/MultiplayerPlaylist.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index aebe70f95a..8ab6531300 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -40,11 +40,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist queueList = new MultiplayerQueueList { RelativeSizeAxes = Axes.Both, + SelectedItem = { BindTarget = SelectedItem } }, historyList = new MultiplayerHistoryList { RelativeSizeAxes = Axes.Both, Alpha = 0, + SelectedItem = { BindTarget = SelectedItem } } } } From 93a7726f4a39410dfcc38cf14a0ebd9126acca75 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 20:24:14 +0900 Subject: [PATCH 124/419] Remove now-unused parameter --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs | 2 +- .../Multiplayer/Match/Playlist/MultiplayerHistoryList.cs | 2 +- .../Multiplayer/Match/Playlist/MultiplayerQueueList.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 3592a51b4a..14c5bd5240 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -20,7 +20,7 @@ namespace osu.Game.Screens.OnlinePlay private readonly bool allowSelection; private readonly bool showItemOwner; - public DrawableRoomPlaylist(bool allowEdit, bool allowSelection, bool reverse = false, bool showItemOwner = false) + public DrawableRoomPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false) { this.allowEdit = allowEdit; this.allowSelection = allowSelection; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs index 76bb364f3a..63b2925c41 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist public class MultiplayerHistoryList : DrawableRoomPlaylist { public MultiplayerHistoryList() - : base(false, false, false, true) + : base(false, false, true) { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index 802603d7e1..7292e2edca 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist public readonly Bindable QueueMode = new Bindable(); public MultiplayerQueueList() - : base(false, false, false, true) + : base(false, false, true) { } From e2f289eeffa1d97378aa9800ba4641b31d61bd34 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 20:27:47 +0900 Subject: [PATCH 125/419] Xmldocs --- .../Multiplayer/Match/Playlist/MultiplayerHistoryList.cs | 3 +++ .../Multiplayer/Match/Playlist/MultiplayerPlaylist.cs | 5 ++++- .../Match/Playlist/MultiplayerPlaylistDisplayMode.cs | 3 +++ .../Multiplayer/Match/Playlist/MultiplayerQueueList.cs | 3 +++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs index 63b2925c41..76088180c4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs @@ -10,6 +10,9 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist { + /// + /// A historically-ordered list of s. + /// public class MultiplayerHistoryList : DrawableRoomPlaylist { public MultiplayerHistoryList() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index 8ab6531300..38199532e2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -7,10 +7,14 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist { + /// + /// The multiplayer playlist, containing lists to show the items from a in both gameplay-order and historical-order. + /// public class MultiplayerPlaylist : MultiplayerRoomComposite { public readonly Bindable DisplayMode = new Bindable(); @@ -56,7 +60,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist protected override void LoadComplete() { base.LoadComplete(); - DisplayMode.BindValueChanged(onDisplayModeChanged, true); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistDisplayMode.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistDisplayMode.cs index af1fac1c79..cc3dca6a34 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistDisplayMode.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistDisplayMode.cs @@ -3,6 +3,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist { + /// + /// The type of list displayed in a . + /// public enum MultiplayerPlaylistDisplayMode { Queue, diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index 7292e2edca..c1bc60becc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -13,6 +13,9 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist { + /// + /// A gameplay-ordered list of s. + /// public class MultiplayerQueueList : DrawableRoomPlaylist { public readonly Bindable QueueMode = new Bindable(); From ad35f3434b210de9de114b88516cf192311fe3e3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 20:53:29 +0900 Subject: [PATCH 126/419] Fix queue list not considering expired items --- .../TestSceneMultiplayerQueueList.cs | 8 ++-- .../Match/Playlist/MultiplayerQueueList.cs | 37 ++++++++++++------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 2e24d54b59..efb4a395f4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -15,13 +15,13 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMultiplayerQueueList : OsuTestScene + public class TestSceneMultiplayerQueueList : MultiplayerTestScene { private MultiplayerQueueList list; private int currentItemId; [SetUp] - public void Setup() => Schedule(() => + public new void Setup() => Schedule(() => { Child = list = new MultiplayerQueueList { @@ -34,8 +34,10 @@ namespace osu.Game.Tests.Visual.Multiplayer }); [SetUpSteps] - public void SetUpSteps() + public override void SetUpSteps() { + base.SetUpSteps(); + // Not inside a step since this is used to affect steps added by the current test. currentItemId = 0; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index c1bc60becc..9184cf6aba 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -35,6 +36,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist { public readonly IBindable QueueMode = new Bindable(); + [Resolved(typeof(Room), nameof(Room.Playlist))] + private BindableList roomPlaylist { get; set; } + protected override void LoadComplete() { base.LoadComplete(); @@ -53,20 +57,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist .OrderBy(item => item.Model.ID); case Game.Online.Multiplayer.QueueMode.AllPlayersRoundRobin: - RearrangeableListItem[] allItems = AliveInternalChildren - .Where(d => d.IsPresent) - .OfType>() - .OrderBy(item => item.Model.ID) - .ToArray(); + RearrangeableListItem[] items = AliveInternalChildren + .Where(d => d.IsPresent) + .OfType>() + .OrderBy(item => item.Model.ID) + .ToArray(); - int totalCount = allItems.Length; + int totalCount = items.Length; if (totalCount == 0) return Enumerable.Empty(); - Dictionary perUserCounts = allItems - .Select(item => item.Model.OwnerID) - .Distinct() - .ToDictionary(u => u, _ => 0); + // Count of "expired" items per user. + Dictionary perUserCounts = roomPlaylist + .Where(item => item.Expired) + .GroupBy(item => item.OwnerID) + .ToDictionary(group => group.Key, group => group.Count()); + + // Fill the count dictionary with zeroes for all users with no currently expired items. + foreach (var item in items) + perUserCounts.TryAdd(item.Model.OwnerID, 0); List result = new List(); @@ -76,16 +85,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist // Todo: This could probably be more efficient, likely at the cost of increased complexity. while (totalCount-- > 0) { - var candidateItem = allItems + var candidateItem = items .Where(item => item != null) .OrderBy(item => perUserCounts[item.Model.OwnerID]) .First(); - int itemIndex = Array.IndexOf(allItems, candidateItem); + int itemIndex = Array.IndexOf(items, candidateItem); - result.Add(allItems[itemIndex]); + result.Add(items[itemIndex]); perUserCounts[candidateItem.Model.OwnerID]++; - allItems[itemIndex] = null; + items[itemIndex] = null; } return result; From e87b0003fb01c89a70586dcf7ce16bda22b1f685 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 20:54:10 +0900 Subject: [PATCH 127/419] Fix queue mode not being bound to in all cases --- .../Multiplayer/TestSceneMultiplayerQueueList.cs | 2 +- .../Match/Playlist/MultiplayerQueueList.cs | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index efb4a395f4..9e002a7f71 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -181,6 +181,6 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } - private void changeQueueModeStep(QueueMode newMode) => AddStep($"change queue mode to {newMode}", () => list.QueueMode.Value = newMode); + private void changeQueueModeStep(QueueMode newMode) => AddStep($"change queue mode to {newMode}", () => SelectedRoom.Value.QueueMode.Value = newMode); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index 9184cf6aba..614e7133fc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -19,8 +19,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist /// public class MultiplayerQueueList : DrawableRoomPlaylist { - public readonly Bindable QueueMode = new Bindable(); - public MultiplayerQueueList() : base(false, false, true) { @@ -28,13 +26,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist protected override FillFlowContainer> CreateListFillFlowContainer() => new QueueFillFlowContainer { - QueueMode = { BindTarget = QueueMode }, Spacing = new Vector2(0, 2) }; private class QueueFillFlowContainer : FillFlowContainer> { - public readonly IBindable QueueMode = new Bindable(); + [Resolved(typeof(Room), nameof(Room.QueueMode))] + private Bindable queueMode { get; set; } [Resolved(typeof(Room), nameof(Room.Playlist))] private BindableList roomPlaylist { get; set; } @@ -42,21 +40,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist protected override void LoadComplete() { base.LoadComplete(); - QueueMode.BindValueChanged(_ => InvalidateLayout()); + queueMode.BindValueChanged(_ => InvalidateLayout()); } public override IEnumerable FlowingChildren { get { - switch (QueueMode.Value) + switch (queueMode.Value) { default: return AliveInternalChildren.Where(d => d.IsPresent) .OfType>() .OrderBy(item => item.Model.ID); - case Game.Online.Multiplayer.QueueMode.AllPlayersRoundRobin: + case QueueMode.AllPlayersRoundRobin: RearrangeableListItem[] items = AliveInternalChildren .Where(d => d.IsPresent) .OfType>() From f9b4e6f004f8d8e12dfb295e055ae6fb8c07983b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 21:00:00 +0900 Subject: [PATCH 128/419] Add test considering expired items --- .../TestSceneMultiplayerQueueList.cs | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 9e002a7f71..bd513ddf64 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -146,21 +146,51 @@ namespace osu.Game.Tests.Visual.Multiplayer } } + [Test] + public void TestPreviouslyExpiredItemsConsideredInRoundRobinMode() + { + changeQueueModeStep(QueueMode.AllPlayersRoundRobin); + + // User 1. + + addItemStep(1, true); + addItemStep(1, true); + PlaylistItem item3 = addItemStep(1); + PlaylistItem item4 = addItemStep(1); + + // User2. + + PlaylistItem item5 = addItemStep(2); + PlaylistItem item6 = addItemStep(2); + + assertPositionStep(item5, 0); + assertPositionStep(item6, 1); + assertPositionStep(item3, 2); + assertPositionStep(item4, 3); + } + /// /// Adds a step to create a new playlist item. /// /// The item owner. + /// Whether the item should be added in an expired state. /// The playlist item's ID. - private PlaylistItem addItemStep(int ownerId) + private PlaylistItem addItemStep(int ownerId, bool expired = false) { var item = new PlaylistItem { ID = ++currentItemId, OwnerID = ownerId, - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo, false).BeatmapInfo } + Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo, false).BeatmapInfo }, + Expired = expired }; - AddStep($"add {{ item: {item.ID}, user: {ownerId} }}", () => list.Items.Add(item)); + AddStep($"add {{ item: {item.ID}, user: {ownerId} }}", () => + { + SelectedRoom.Value.Playlist.Add(item); + if (!expired) + list.Items.Add(item); + }); return item; } From e5e2ae8ab4f682addd7b8ad3cce7d7d61a39f422 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Dec 2021 21:36:25 +0900 Subject: [PATCH 129/419] Fix dangling line post-rebase --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 14c5bd5240..f2d31c8e67 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -25,8 +25,6 @@ namespace osu.Game.Screens.OnlinePlay this.allowEdit = allowEdit; this.allowSelection = allowSelection; this.showItemOwner = showItemOwner; - - ((ReversibleFillFlowContainer)ListContainer).Reverse = reverse; } protected override void LoadComplete() From a0ff86f5e80055ac91cb63eb456899b6d7b4a139 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 22:43:03 +0900 Subject: [PATCH 130/419] Ensure all read and write operations on `APIRoom` are done on the update thread --- .../Online/Multiplayer/MultiplayerClient.cs | 32 ++++++++++--------- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 10 ++++-- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 60a7dda961..82844af3c5 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -444,18 +444,28 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } - async Task IMultiplayerClient.SettingsChanged(MultiplayerRoomSettings newSettings) + Task IMultiplayerClient.SettingsChanged(MultiplayerRoomSettings newSettings) { Debug.Assert(APIRoom != null); Debug.Assert(Room != null); - // ensure the new selected item is populated immediately. - var playlistItem = APIRoom.Playlist.SingleOrDefault(p => p.ID == newSettings.PlaylistItemId); + Scheduler.Add(() => + { + // ensure the new selected item is populated immediately. + var playlistItem = APIRoom.Playlist.SingleOrDefault(p => p.ID == newSettings.PlaylistItemId); - if (playlistItem != null) - await PopulateBeatmap(playlistItem).ConfigureAwait(false); + if (playlistItem != null) + { + GetAPIBeatmap(playlistItem.BeatmapID).ContinueWith(b => + { + Scheduler.Add(() => playlistItem.Beatmap.Value = b.Result); + }, TaskContinuationOptions.OnlyOnRanToCompletion); + } - Scheduler.Add(() => updateLocalRoomSettings(newSettings)); + updateLocalRoomSettings(newSettings); + }); + + return Task.CompletedTask; } Task IMultiplayerClient.UserStateChanged(int userId, MultiplayerUserState state) @@ -727,19 +737,11 @@ namespace osu.Game.Online.Multiplayer playlistItem.AllowedMods.AddRange(item.AllowedMods.Select(m => m.ToMod(rulesetInstance))); if (populateBeatmapImmediately) - await PopulateBeatmap(playlistItem).ConfigureAwait(false); + playlistItem.Beatmap.Value = await GetAPIBeatmap(item.BeatmapID).ConfigureAwait(false); return playlistItem; } - public async Task PopulateBeatmap(PlaylistItem item) - { - if (item.Beatmap.Value != null) - return; - - item.Beatmap.Value = await GetAPIBeatmap(item.BeatmapID).ConfigureAwait(false); - } - /// /// Retrieves a from an online source. /// diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index e4b0d9647e..2e630262f3 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -69,6 +69,9 @@ namespace osu.Game.Screens.OnlinePlay [Resolved] private UserLookupCache userLookupCache { get; set; } + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } + [Resolved] private MultiplayerClient multiplayerClient { get; set; } @@ -148,10 +151,11 @@ namespace osu.Game.Screens.OnlinePlay { try { - var user = await userLookupCache.GetUserAsync(Item.OwnerID).ConfigureAwait(false); - Schedule(() => ownerAvatar.User = user); + var foundUser = await userLookupCache.GetUserAsync(Item.OwnerID).ConfigureAwait(false); + Schedule(() => ownerAvatar.User = foundUser); - await multiplayerClient.PopulateBeatmap(Item).ConfigureAwait(false); + var foundBeatmap = await beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ConfigureAwait(false); + Schedule(() => Item.Beatmap.Value = foundBeatmap); } catch (Exception e) { From d262baefada800347338f6422739baa1b50bc45a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 22:43:32 +0900 Subject: [PATCH 131/419] Only query for the owner user metadata in the case it is actually required --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 2e630262f3..e2471d71d6 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -151,8 +151,11 @@ namespace osu.Game.Screens.OnlinePlay { try { - var foundUser = await userLookupCache.GetUserAsync(Item.OwnerID).ConfigureAwait(false); - Schedule(() => ownerAvatar.User = foundUser); + if (showItemOwner) + { + var foundUser = await userLookupCache.GetUserAsync(Item.OwnerID).ConfigureAwait(false); + Schedule(() => ownerAvatar.User = foundUser); + } var foundBeatmap = await beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ConfigureAwait(false); Schedule(() => Item.Beatmap.Value = foundBeatmap); From a8e17cb3a5bad201deb8d2df3a07412e613d7282 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 22:46:32 +0900 Subject: [PATCH 132/419] Add relative size specs so partially on-screen panels still start loading Without this, panels at the top of the list but not fully on-screen wouldn't begin their metadata loading process. --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index e2471d71d6..dae7b611f2 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay private PanelBackground panelBackground; - private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty); + private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both }; private readonly bool allowEdit; private readonly bool allowSelection; From 8bef50cbbaf79a3918f242643f412822bcd313e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 1 Dec 2021 19:35:15 +0100 Subject: [PATCH 133/419] Revert "Refactor migration precondition to read better" Realm cannot translate `.All()` LINQ queries. This reverts commit 0e0e8c25e8587e0fd121d1c4a0ad8cfe9055a08f. --- osu.Game/Database/EFToRealmMigrator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index 8f436470af..3790dc8ae9 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -63,7 +63,8 @@ namespace osu.Game.Database using (var transaction = realm.BeginWrite()) { // only migrate data if the realm database is empty. - if (realm.All().All(s => s.Protected)) + // note that this cannot be written as: `realm.All().All(s => s.Protected)`, because realm does not support `.All()`. + if (!realm.All().Any(s => !s.Protected)) { foreach (var skin in existingSkins) { From 4f826589e51fc504db7f0ece73fa94e2e9da374c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Dec 2021 12:20:07 +0900 Subject: [PATCH 134/419] Remove subscription logic for the time being --- .../LegacyDatabasedSkinResourceStore.cs | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs b/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs index 667dc1876b..cd90fea9bb 100644 --- a/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs +++ b/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs @@ -1,14 +1,10 @@ // 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 osu.Framework.Development; using osu.Framework.Extensions; using osu.Framework.IO.Stores; using osu.Game.Extensions; -using Realms; namespace osu.Game.Skinning { @@ -16,38 +12,12 @@ namespace osu.Game.Skinning { private readonly Dictionary fileToStoragePathMapping = new Dictionary(); - private readonly IDisposable subscription; - public LegacyDatabasedSkinResourceStore(SkinInfo source, IResourceStore underlyingStore) : base(underlyingStore) { - // Subscribing to non-managed instances doesn't work. - // In this usage, the skin may be non-managed in tests. - if (source.IsManaged) - { - // Subscriptions can only work on the main thread. - Debug.Assert(ThreadSafety.IsUpdateThread); - - subscription = source.Files - .AsRealmCollection().SubscribeForNotifications((sender, changes, error) => - { - if (changes == null) - return; - - // If a large number of changes are made on skin files, this may be better suited to being cleared here - // and reinitialised on next usage. - initialiseFileCache(source); - }); - } - initialiseFileCache(source); } - ~LegacyDatabasedSkinResourceStore() - { - Dispose(false); - } - private void initialiseFileCache(SkinInfo source) { fileToStoragePathMapping.Clear(); @@ -65,13 +35,6 @@ namespace osu.Game.Skinning } } - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - subscription?.Dispose(); - } - private string getPathForFile(string filename) => fileToStoragePathMapping.TryGetValue(filename.ToLower(), out string path) ? path : null; From c82195390fb19ed1e69121271ce4b91ea870f954 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Dec 2021 13:24:16 +0900 Subject: [PATCH 135/419] Update usage of `SubscribeForNotifications` --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index e3230098e2..c932f4837a 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -18,7 +18,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Skinning; using osu.Game.Skinning.Editor; -using Realms; namespace osu.Game.Overlays.Settings.Sections { @@ -85,7 +84,7 @@ namespace osu.Game.Overlays.Settings.Sections .ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase); realmSubscription = realmSkins - .SubscribeForNotifications((sender, changes, error) => + .QueryAsyncWithNotifications((sender, changes, error) => { if (changes == null) return; From 0a14acfd83d636ab2f7a087b6c4efcd134e7ea46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Dec 2021 13:41:20 +0900 Subject: [PATCH 136/419] Fix incorrect conditional on export/mutate feasability of skin --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 2 +- osu.Game/Skinning/SkinManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index c932f4837a..bcf0a367ef 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -179,7 +179,7 @@ namespace osu.Game.Overlays.Settings.Sections base.LoadComplete(); currentSkin = skins.CurrentSkin.GetBoundCopy(); - currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.PerformRead(s => s.IsManaged), true); + currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.PerformRead(s => !s.Protected), true); } private void export() diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index d6c884d259..701bf6d0cf 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -144,7 +144,7 @@ namespace osu.Game.Skinning { CurrentSkinInfo.Value.PerformRead(s => { - if (s.IsManaged) + if (!s.Protected) return; // if the user is attempting to save one of the default skin implementations, create a copy first. From e855a49833fb0a08694233757641e8828f5d3955 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Dec 2021 14:01:18 +0900 Subject: [PATCH 137/419] Add test coverage of default skin edit and export --- .../Navigation/TestSceneEditDefaultSkin.cs | 40 +++++++++++++++++++ .../Overlays/Settings/Sections/SkinSection.cs | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs b/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs new file mode 100644 index 0000000000..f5b9d287ca --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.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 System.IO; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Testing; +using osu.Game.Overlays.Settings.Sections; +using osu.Game.Skinning; +using osu.Game.Skinning.Editor; + +namespace osu.Game.Tests.Visual.Navigation +{ + public class TestSceneEditDefaultSkin : OsuGameTestScene + { + private SkinManager skinManager => Game.Dependencies.Get(); + private SkinEditorOverlay skinEditor => Game.Dependencies.Get(); + + [Test] + public void TestEditDefaultSkin() + { + AddAssert("is default skin", () => skinManager.CurrentSkinInfo.Value.ID == SkinInfo.DEFAULT_SKIN); + + AddStep("open settings", () => { Game.Settings.Show(); }); + + // Until step requires as settings has a delayed load. + AddUntilStep("export button disabled", () => Game.Settings.ChildrenOfType().SingleOrDefault()?.Enabled.Value == false); + + // Will create a mutable skin. + AddStep("open skin editor", () => skinEditor.Show()); + + // Until step required as the skin editor may take time to load (and an extra scheduled frame for the mutable part). + AddUntilStep("is modified default skin", () => skinManager.CurrentSkinInfo.Value.ID != SkinInfo.DEFAULT_SKIN); + AddAssert("is not protected", () => skinManager.CurrentSkinInfo.Value.PerformRead(s => !s.Protected)); + + AddUntilStep("export button enabled", () => Game.Settings.ChildrenOfType().SingleOrDefault()?.Enabled.Value == true); + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index bcf0a367ef..b1582d7bee 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -157,7 +157,7 @@ namespace osu.Game.Overlays.Settings.Sections } } - private class ExportSkinButton : SettingsButton + public class ExportSkinButton : SettingsButton { [Resolved] private SkinManager skins { get; set; } From ec700e91427549ff29a36ba9300a8e1aa94d2410 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Dec 2021 14:01:57 +0900 Subject: [PATCH 138/419] Remove unused using statement --- osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs b/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs index f5b9d287ca..06306ad197 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; From 624ec4580a1b5347224b073323cb6a39adb6ea5f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Dec 2021 14:31:59 +0900 Subject: [PATCH 139/419] Ensure `updateLocalRoomSettings` is only called after full population --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 82844af3c5..c2b9727309 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -458,11 +458,17 @@ namespace osu.Game.Online.Multiplayer { GetAPIBeatmap(playlistItem.BeatmapID).ContinueWith(b => { - Scheduler.Add(() => playlistItem.Beatmap.Value = b.Result); - }, TaskContinuationOptions.OnlyOnRanToCompletion); - } + bool success = b.IsCompletedSuccessfully; - updateLocalRoomSettings(newSettings); + Scheduler.Add(() => + { + if (success) + playlistItem.Beatmap.Value = b.Result; + + updateLocalRoomSettings(newSettings); + }); + }); + } }); return Task.CompletedTask; From bdddaba3521f524a150d41505efcbf8ee6fb0020 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Dec 2021 14:31:08 +0900 Subject: [PATCH 140/419] Remove unnecessary test request handling --- .../Visual/OnlinePlay/TestRoomRequestsHandler.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index abcf31c007..af2202b05a 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -4,9 +4,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; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Scoring; @@ -89,15 +87,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay getRoomRequest.TriggerSuccess(createResponseRoom(ServerSideRooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId), true)); return true; - 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); - return true; - case CreateRoomScoreRequest createRoomScoreRequest: createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 }); return true; From 0adfb75cf36decb00fec3ac4189d38513b366408 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Dec 2021 14:53:06 +0900 Subject: [PATCH 141/419] Combine similarly named `StatefulMultiplayerClient` tests --- .../StatefulMultiplayerClientTest.cs | 21 +++++++++++ .../StatefulMultiplayerClientTest.cs | 35 ------------------- 2 files changed, 21 insertions(+), 35 deletions(-) delete mode 100644 osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 840ff20a83..42305ccd81 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -16,6 +16,27 @@ namespace osu.Game.Tests.NonVisual.Multiplayer [HeadlessTest] public class StatefulMultiplayerClientTest : MultiplayerTestScene { + [Test] + public void TestUserAddedOnJoin() + { + var user = new APIUser { Id = 33 }; + + AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3); + AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); + } + + [Test] + public void TestUserRemovedOnLeave() + { + var user = new APIUser { Id = 44 }; + + AddStep("add user", () => Client.AddUser(user)); + AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); + + AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3); + AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1); + } + [Test] public void TestPlayingUserTracking() { diff --git a/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs deleted file mode 100644 index 5a621ecf84..0000000000 --- a/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using NUnit.Framework; -using osu.Framework.Testing; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Tests.Visual.Multiplayer; - -namespace osu.Game.Tests.OnlinePlay -{ - [HeadlessTest] - public class StatefulMultiplayerClientTest : MultiplayerTestScene - { - [Test] - public void TestUserAddedOnJoin() - { - var user = new APIUser { Id = 33 }; - - AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3); - AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); - } - - [Test] - public void TestUserRemovedOnLeave() - { - var user = new APIUser { Id = 44 }; - - AddStep("add user", () => Client.AddUser(user)); - AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); - - AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3); - AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1); - } - } -} From 5976982b12c158282a67b97844965dc98a989d39 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Dec 2021 16:45:26 +0900 Subject: [PATCH 142/419] Add missing xmldoc for `MultiplayerClient` events --- .../Online/Multiplayer/MultiplayerClient.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 784d365520..8ac2de9902 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -32,12 +32,34 @@ namespace osu.Game.Online.Multiplayer /// public event Action? RoomUpdated; + /// + /// Invoked when a new user joins the room. + /// public event Action? UserJoined; + + /// + /// Invoked when a user leaves the room of their own accord. + /// public event Action? UserLeft; + + /// + /// Invoked when a user was kicked from the room forcefully. + /// public event Action? UserKicked; + /// + /// Invoked when a new item is added to the playlist. + /// public event Action? ItemAdded; + + /// + /// Invoked when a playlist item is removed from the playlist. The provided long is the playlist's item ID. + /// public event Action? ItemRemoved; + + /// + /// Invoked when a playlist item's details change. + /// public event Action? ItemChanged; /// From 512818648f0f0fd2efe4df15e9231b20d15c96d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Dec 2021 16:56:30 +0900 Subject: [PATCH 143/419] Add some more breathing room between tab control and queue content --- .../Multiplayer/Match/Playlist/MultiplayerPlaylist.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index 38199532e2..2c50c88de8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -26,18 +26,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist [BackgroundDependencyLoader] private void load() { + const float tab_control_height = 25; + InternalChildren = new Drawable[] { new OsuTabControl { RelativeSizeAxes = Axes.X, - Height = 25, + Height = tab_control_height, Current = { BindTarget = DisplayMode } }, new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 27 }, + Padding = new MarginPadding { Top = tab_control_height + 5 }, Masking = true, Children = new Drawable[] { From ae3038ead4afb1329f765eca71dbdfa3dcfc78d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Dec 2021 17:17:12 +0900 Subject: [PATCH 144/419] Overwrite existing files if `AddFile` is called with an existing filename --- osu.Game/Database/IModelFileManager.cs | 2 +- osu.Game/Stores/RealmArchiveModelManager.cs | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/IModelFileManager.cs b/osu.Game/Database/IModelFileManager.cs index 4bc1e2d29b..390be4a69d 100644 --- a/osu.Game/Database/IModelFileManager.cs +++ b/osu.Game/Database/IModelFileManager.cs @@ -25,7 +25,7 @@ namespace osu.Game.Database void DeleteFile(TModel model, TFileModel file); /// - /// Add a new file. + /// Add a new file. If the file already exists, it is overwritten. /// /// The item to operate on. /// The new file contents. diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index a916e2b53a..4efcdd096c 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -74,10 +74,18 @@ namespace osu.Game.Stores } /// - /// Add a file from within an ongoing realm transaction. + /// Add a file from within an ongoing realm transaction. If the file already exists, it is overwritten. /// protected void AddFile(TModel item, Stream stream, string filename, Realm realm) { + var existing = item.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase)); + + if (existing != null) + { + ReplaceFile(item, existing, stream, realm); + return; + } + var file = realmFileStore.Add(stream, realm); var namedUsage = new RealmNamedFileUsage(file, filename); From fe99d4e984c7fad82bfb12e54ebcbd03d1835f55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Dec 2021 17:19:53 +0900 Subject: [PATCH 145/419] Standardise parameter naming across all file IO methods --- osu.Game/Skinning/SkinModelManager.cs | 4 ++-- osu.Game/Stores/RealmArchiveModelManager.cs | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Skinning/SkinModelManager.cs b/osu.Game/Skinning/SkinModelManager.cs index 059345f9bc..40c280c5c2 100644 --- a/osu.Game/Skinning/SkinModelManager.cs +++ b/osu.Game/Skinning/SkinModelManager.cs @@ -128,7 +128,7 @@ namespace osu.Game.Skinning sw.WriteLine(line); } - ReplaceFile(item, existingFile, stream, realm); + ReplaceFile(existingFile, stream, realm); // can be removed 20220502. if (!ensureIniWasUpdated(item)) @@ -214,7 +214,7 @@ namespace osu.Game.Skinning var oldFile = s.Files.FirstOrDefault(f => f.Filename == filename); if (oldFile != null) - ReplaceFile(s, oldFile, streamContent, s.Realm); + ReplaceFile(oldFile, streamContent, s.Realm); else AddFile(s, streamContent, filename, s.Realm); } diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index 4efcdd096c..87a27cbbbc 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -52,10 +52,10 @@ namespace osu.Game.Stores item.Realm.Write(() => DeleteFile(item, file, item.Realm)); public void ReplaceFile(TModel item, RealmNamedFileUsage file, Stream contents) - => item.Realm.Write(() => ReplaceFile(item, file, contents, item.Realm)); + => item.Realm.Write(() => ReplaceFile(file, contents, item.Realm)); - public void AddFile(TModel item, Stream stream, string filename) - => item.Realm.Write(() => AddFile(item, stream, filename, item.Realm)); + public void AddFile(TModel item, Stream contents, string filename) + => item.Realm.Write(() => AddFile(item, contents, filename, item.Realm)); /// /// Delete a file from within an ongoing realm transaction. @@ -68,7 +68,7 @@ namespace osu.Game.Stores /// /// Replace a file from within an ongoing realm transaction. /// - protected void ReplaceFile(TModel model, RealmNamedFileUsage file, Stream contents, Realm realm) + protected void ReplaceFile(RealmNamedFileUsage file, Stream contents, Realm realm) { file.File = realmFileStore.Add(contents, realm); } @@ -76,17 +76,17 @@ namespace osu.Game.Stores /// /// Add a file from within an ongoing realm transaction. If the file already exists, it is overwritten. /// - protected void AddFile(TModel item, Stream stream, string filename, Realm realm) + protected void AddFile(TModel item, Stream contents, string filename, Realm realm) { var existing = item.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase)); if (existing != null) { - ReplaceFile(item, existing, stream, realm); + ReplaceFile(existing, contents, realm); return; } - var file = realmFileStore.Add(stream, realm); + var file = realmFileStore.Add(contents, realm); var namedUsage = new RealmNamedFileUsage(file, filename); item.Files.Add(namedUsage); From 0e82e9355bd0072979459e11454fcfdd2f926c88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Dec 2021 17:42:16 +0900 Subject: [PATCH 146/419] Ensure skin is saved immediately after becoming mutable Without doing this, the JSON content is not written to the file. A user assumption is that as soon as a skin shows up in the skin list as exportable, it should export correctly, so it makes sense that it should be in a sane state even if the user has not made any changes in the skin editor yet. Going forward, we might move more of the json serialisation logic out, and run for consistency as part of the import process. This seems like the simplest way to guarantee things for now, though. --- osu.Game/Skinning/SkinManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 701bf6d0cf..5134632fb1 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -156,7 +156,13 @@ namespace osu.Game.Skinning }).Result; if (result != null) + { + // save once to ensure the required json content is populated. + // currently this only happens on save. + result.PerformRead(skin => Save(skin.CreateInstance(this))); + CurrentSkinInfo.Value = result; + } }); } From b9768487742c254866f4460adc808cc580cd6a16 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Dec 2021 18:00:04 +0900 Subject: [PATCH 147/419] Add failing test coverage of exporting default skin importing with incorrect type --- osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 68 +++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index f3c2cd8ba2..f2ce002650 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -164,6 +164,74 @@ namespace osu.Game.Tests.Skins.IO assertCorrectMetadata(import2, "name 1 [my custom skin 2]", "author 1", osu); }); + [Test] + public Task TestExportThenImportDefaultSkin() => runSkinTest(osu => + { + var skinManager = osu.Dependencies.Get(); + + skinManager.EnsureMutableSkin(); + + MemoryStream exportStream = new MemoryStream(); + + Guid originalSkinId = skinManager.CurrentSkinInfo.Value.ID; + + skinManager.CurrentSkinInfo.Value.PerformRead(s => + { + Assert.IsFalse(s.Protected); + Assert.AreEqual(typeof(DefaultSkin), s.CreateInstance(skinManager).GetType()); + + new LegacySkinExporter(osu.Dependencies.Get()).ExportModelTo(s, exportStream); + + Assert.Greater(exportStream.Length, 0); + }); + + var imported = skinManager.Import(new ImportTask(exportStream, "exported.osk")); + + imported.Result.PerformRead(s => + { + Assert.IsFalse(s.Protected); + Assert.AreNotEqual(originalSkinId, s.ID); + Assert.AreEqual(typeof(DefaultSkin), s.CreateInstance(skinManager).GetType()); + }); + + return Task.CompletedTask; + }); + + [Test] + public Task TestExportThenImportClassicSkin() => runSkinTest(osu => + { + var skinManager = osu.Dependencies.Get(); + + skinManager.CurrentSkinInfo.Value = skinManager.DefaultLegacySkin.SkinInfo; + + skinManager.EnsureMutableSkin(); + + MemoryStream exportStream = new MemoryStream(); + + Guid originalSkinId = skinManager.CurrentSkinInfo.Value.ID; + + skinManager.CurrentSkinInfo.Value.PerformRead(s => + { + Assert.IsFalse(s.Protected); + Assert.AreEqual(typeof(DefaultLegacySkin), s.CreateInstance(skinManager).GetType()); + + new LegacySkinExporter(osu.Dependencies.Get()).ExportModelTo(s, exportStream); + + Assert.Greater(exportStream.Length, 0); + }); + + var imported = skinManager.Import(new ImportTask(exportStream, "exported.osk")); + + imported.Result.PerformRead(s => + { + Assert.IsFalse(s.Protected); + Assert.AreNotEqual(originalSkinId, s.ID); + Assert.AreEqual(typeof(DefaultLegacySkin), s.CreateInstance(skinManager).GetType()); + }); + + return Task.CompletedTask; + }); + #endregion private void assertCorrectMetadata(ILive import1, string name, string creator, OsuGameBase osu) From cdf2fa99304759414ff11db461508ce32c3f616f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Dec 2021 17:43:54 +0900 Subject: [PATCH 148/419] Serialise and deserialise `SkinInfo.InstantiationInfo` to allow for more correct imports Until now, skins were always imported using the `LegacySkin` instantiation type. For cases where a user has edited the lazer or classic default (via the new skin editor), which would result in incorrect fallback paths after exporting and importing the edited skin. --- osu.Game/Skinning/SkinModelManager.cs | 43 +++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/SkinModelManager.cs b/osu.Game/Skinning/SkinModelManager.cs index 40c280c5c2..822cb8efa0 100644 --- a/osu.Game/Skinning/SkinModelManager.cs +++ b/osu.Game/Skinning/SkinModelManager.cs @@ -24,6 +24,8 @@ namespace osu.Game.Skinning { public class SkinModelManager : RealmArchiveModelManager { + private const string skin_info_file = "skininfo.json"; + private readonly IStorageResourceProvider skinResources; public SkinModelManager(Storage storage, RealmContextFactory contextFactory, GameHost host, IStorageResourceProvider skinResources) @@ -49,8 +51,36 @@ namespace osu.Game.Skinning protected override Task Populate(SkinInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) { - if (string.IsNullOrEmpty(model.InstantiationInfo)) - model.InstantiationInfo = createInstance(model).GetType().GetInvariantInstantiationInfo(); + var skinInfoFile = model.Files.SingleOrDefault(f => f.Filename == skin_info_file); + + if (skinInfoFile != null) + { + try + { + using (var existingStream = Files.Storage.GetStream(skinInfoFile.File.GetStoragePath())) + using (var reader = new StreamReader(existingStream)) + { + var deserialisedSkinInfo = JsonConvert.DeserializeObject(reader.ReadToEnd()); + + if (deserialisedSkinInfo != null) + { + // for now we only care about the instantiation info. + // eventually we probably want to transfer everything across. + model.InstantiationInfo = deserialisedSkinInfo.InstantiationInfo; + } + } + } + catch (Exception e) + { + LogForModel(model, $"Error during {skin_info_file} parsing, falling back to default", e); + + // Not sure if we should still run the import in the case of failure here, but let's do so for now. + model.InstantiationInfo = string.Empty; + } + } + + // Always rewrite instantiation info (even after parsing in from the skin json) for sanity. + model.InstantiationInfo = createInstance(model).GetType().GetInvariantInstantiationInfo(); checkSkinIniMetadata(model, realm); @@ -203,6 +233,15 @@ namespace osu.Game.Skinning { skin.SkinInfo.PerformWrite(s => { + // Serialise out the SkinInfo itself. + string skinInfoJson = JsonConvert.SerializeObject(s, new JsonSerializerSettings { Formatting = Formatting.Indented }); + + using (var streamContent = new MemoryStream(Encoding.UTF8.GetBytes(skinInfoJson))) + { + AddFile(s, streamContent, skin_info_file, s.Realm); + } + + // Then serialise each of the drawable component groups into respective files. foreach (var drawableInfo in skin.DrawableComponentInfo) { string json = JsonConvert.SerializeObject(drawableInfo.Value, new JsonSerializerSettings { Formatting = Formatting.Indented }); From ba8af303ccc0e2e9053f22034be7baa4b86dbdc7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 2 Dec 2021 19:15:27 +0900 Subject: [PATCH 149/419] Add GameplayOrder to MultiplayerPlaylistItem --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 3 ++- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 10 ++++++++++ osu.Game/Online/Rooms/PlaylistItem.cs | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 8ac2de9902..0ffb81d986 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -741,7 +741,8 @@ namespace osu.Game.Online.Multiplayer OwnerID = item.OwnerID, Beatmap = { Value = apiBeatmap }, Ruleset = { Value = ruleset }, - Expired = item.Expired + Expired = item.Expired, + GameplayOrder = item.GameplayOrder }; playlistItem.RequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))); diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 6ca0b822f3..5094ee510f 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -39,6 +39,15 @@ namespace osu.Game.Online.Rooms [Key(7)] public bool Expired { get; set; } + /// + /// The order in which this will be played, starting from 0 and increasing for items which will be played later. + /// + /// + /// Undefined value for items which are expired. + /// + [Key(8)] + public int GameplayOrder { get; set; } + public MultiplayerPlaylistItem() { } @@ -52,6 +61,7 @@ namespace osu.Game.Online.Rooms RequiredMods = item.RequiredMods.Select(m => new APIMod(m)).ToArray(); AllowedMods = item.AllowedMods.Select(m => new APIMod(m)).ToArray(); Expired = item.Expired; + GameplayOrder = item.GameplayOrder; } } } diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index a1480865b8..4c7f9e139a 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -33,6 +33,9 @@ namespace osu.Game.Online.Rooms [JsonProperty("expired")] public bool Expired { get; set; } + [JsonProperty("gameplay_order")] + public int GameplayOrder { get; set; } + [JsonIgnore] public IBindable Valid => valid; From 9760a2b08723f326e349bad1d799d7e99f596a23 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 2 Dec 2021 21:13:20 +0900 Subject: [PATCH 150/419] Update MultiplayerQueueList to take advantage of GameplayOrder --- .../Match/Playlist/MultiplayerQueueList.cs | 64 +------------------ 1 file changed, 2 insertions(+), 62 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index 614e7133fc..58f35b940a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -1,14 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osuTK; @@ -31,74 +29,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist private class QueueFillFlowContainer : FillFlowContainer> { - [Resolved(typeof(Room), nameof(Room.QueueMode))] - private Bindable queueMode { get; set; } - [Resolved(typeof(Room), nameof(Room.Playlist))] private BindableList roomPlaylist { get; set; } protected override void LoadComplete() { base.LoadComplete(); - queueMode.BindValueChanged(_ => InvalidateLayout()); + roomPlaylist.BindCollectionChanged((_, __) => InvalidateLayout()); } - public override IEnumerable FlowingChildren - { - get - { - switch (queueMode.Value) - { - default: - return AliveInternalChildren.Where(d => d.IsPresent) - .OfType>() - .OrderBy(item => item.Model.ID); - - case QueueMode.AllPlayersRoundRobin: - RearrangeableListItem[] items = AliveInternalChildren - .Where(d => d.IsPresent) - .OfType>() - .OrderBy(item => item.Model.ID) - .ToArray(); - - int totalCount = items.Length; - if (totalCount == 0) - return Enumerable.Empty(); - - // Count of "expired" items per user. - Dictionary perUserCounts = roomPlaylist - .Where(item => item.Expired) - .GroupBy(item => item.OwnerID) - .ToDictionary(group => group.Key, group => group.Count()); - - // Fill the count dictionary with zeroes for all users with no currently expired items. - foreach (var item in items) - perUserCounts.TryAdd(item.Model.OwnerID, 0); - - List result = new List(); - - // Run a simulation... - // In every iteration, pick the first available item from the user with the lowest number of items in the queue to add to the result set. - // If multiple users have the same number of items in the queue, then the item with the lowest ID is chosen. - // Todo: This could probably be more efficient, likely at the cost of increased complexity. - while (totalCount-- > 0) - { - var candidateItem = items - .Where(item => item != null) - .OrderBy(item => perUserCounts[item.Model.OwnerID]) - .First(); - - int itemIndex = Array.IndexOf(items, candidateItem); - - result.Add(items[itemIndex]); - perUserCounts[candidateItem.Model.OwnerID]++; - items[itemIndex] = null; - } - - return result; - } - } - } + public override IEnumerable FlowingChildren => base.FlowingChildren.OfType>().OrderBy(item => item.Model.GameplayOrder); } } } From 933fd49cff4fb0c9d66535cfab6aa7166d7f097e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 2 Dec 2021 22:11:28 +0900 Subject: [PATCH 151/419] Fix missed callbacks due to AddOnce() schedules --- .../OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs index f2cac708e4..2f75f09a9f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs @@ -35,9 +35,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void invokeUserJoined(MultiplayerRoomUser user) => Scheduler.AddOnce(UserJoined, user); private void invokeUserKicked(MultiplayerRoomUser user) => Scheduler.AddOnce(UserKicked, user); private void invokeUserLeft(MultiplayerRoomUser user) => Scheduler.AddOnce(UserLeft, user); - private void invokeItemAdded(MultiplayerPlaylistItem item) => Scheduler.AddOnce(PlaylistItemAdded, item); - private void invokeItemRemoved(long item) => Scheduler.AddOnce(PlaylistItemRemoved, item); - private void invokeItemChanged(MultiplayerPlaylistItem item) => Scheduler.AddOnce(PlaylistItemChanged, item); + private void invokeItemAdded(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemAdded(item)); + private void invokeItemRemoved(long item) => Schedule(() => PlaylistItemRemoved(item)); + private void invokeItemChanged(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemChanged(item)); /// /// Invoked when a user has joined the room. From 806ca5d4de4f9dac36c1e3a00ff9cc1a0331c13e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 2 Dec 2021 22:32:41 +0900 Subject: [PATCH 152/419] Update TestMultiplayerClient implementation to match server --- .../Multiplayer/TestMultiplayerClient.cs | 102 +++++++++++++----- 1 file changed, 77 insertions(+), 25 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 82a8212a94..2831c94429 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Multiplayer /// /// Guaranteed up-to-date playlist. /// - private readonly List serverSidePlaylist = new List(); + private List serverSidePlaylist = new List(); private MultiplayerPlaylistItem? currentItem => Room?.Playlist[currentIndex]; private int currentIndex; @@ -189,6 +189,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Host = localUser }; + await updatePlaylistOrder(room).ConfigureAwait(false); await updateCurrentItem(room, false).ConfigureAwait(false); RoomSetupAction?.Invoke(room); @@ -308,12 +309,14 @@ namespace osu.Game.Tests.Visual.Multiplayer if (Room.Settings.QueueMode == QueueMode.HostOnly && Room.Host?.UserID != LocalUser?.UserID) throw new InvalidOperationException("Local user is not the room host."); + item.OwnerID = userId; + switch (Room.Settings.QueueMode) { case QueueMode.HostOnly: // In host-only mode, the current item is re-used. item.ID = currentItem.ID; - item.OwnerID = currentItem.OwnerID; + item.GameplayOrder = currentItem.GameplayOrder; serverSidePlaylist[currentIndex] = item; await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); @@ -323,12 +326,9 @@ namespace osu.Game.Tests.Visual.Multiplayer break; default: - item.ID = serverSidePlaylist.Last().ID + 1; - item.OwnerID = userId; - - serverSidePlaylist.Add(item); - await ((IMultiplayerClient)this).PlaylistItemAdded(item).ConfigureAwait(false); + await addItem(item).ConfigureAwait(false); + // The current item can change as a result of an item being added. For example, if all items earlier in the queue were expired. await updateCurrentItem(Room).ConfigureAwait(false); break; } @@ -385,7 +385,7 @@ namespace osu.Game.Tests.Visual.Multiplayer if (newMode == QueueMode.HostOnly && serverSidePlaylist.All(item => item.Expired)) await duplicateCurrentItem().ConfigureAwait(false); - // When changing modes, items could have been added (above) or the queueing order could have changed. + await updatePlaylistOrder(Room).ConfigureAwait(false); await updateCurrentItem(Room).ConfigureAwait(false); } @@ -408,47 +408,99 @@ namespace osu.Game.Tests.Visual.Multiplayer private async Task duplicateCurrentItem() { - Debug.Assert(Room != null); - Debug.Assert(APIRoom != null); Debug.Assert(currentItem != null); - var newItem = new MultiplayerPlaylistItem + await addItem(new MultiplayerPlaylistItem { - ID = serverSidePlaylist.Last().ID + 1, BeatmapID = currentItem.BeatmapID, BeatmapChecksum = currentItem.BeatmapChecksum, RulesetID = currentItem.RulesetID, RequiredMods = currentItem.RequiredMods, AllowedMods = currentItem.AllowedMods - }; + }).ConfigureAwait(false); + } - serverSidePlaylist.Add(newItem); - await ((IMultiplayerClient)this).PlaylistItemAdded(newItem).ConfigureAwait(false); + private async Task addItem(MultiplayerPlaylistItem item) + { + Debug.Assert(Room != null); + + // Add the item to the list first in order to compute gameplay order. + serverSidePlaylist.Add(item); + await updatePlaylistOrder(Room).ConfigureAwait(false); + + item.ID = serverSidePlaylist[^2].ID + 1; + await ((IMultiplayerClient)this).PlaylistItemAdded(item).ConfigureAwait(false); } private async Task updateCurrentItem(MultiplayerRoom room, bool notify = true) { - MultiplayerPlaylistItem newItem; + // The playlist is already in correct gameplay order, so pick the next non-expired item or default to the last item. + MultiplayerPlaylistItem nextItem = serverSidePlaylist.FirstOrDefault(i => !i.Expired) ?? room.Playlist.Last(); + currentIndex = serverSidePlaylist.IndexOf(nextItem); + + long lastItem = room.Settings.PlaylistItemId; + room.Settings.PlaylistItemId = nextItem.ID; + + if (notify && nextItem.ID != lastItem) + await ((IMultiplayerClient)this).SettingsChanged(room.Settings).ConfigureAwait(false); + } + + private async Task updatePlaylistOrder(MultiplayerRoom room) + { + List orderedItems; switch (room.Settings.QueueMode) { default: - // Pick the single non-expired playlist item. - newItem = serverSidePlaylist.FirstOrDefault(i => !i.Expired) ?? serverSidePlaylist.Last(); + orderedItems = serverSidePlaylist.OrderBy(item => item.ID == 0 ? int.MaxValue : item.ID).ToList(); break; case QueueMode.AllPlayersRoundRobin: - // Group playlist items by (user_id -> count_expired), and select the first available playlist item from a user that has available beatmaps where count_expired is the lowest. - throw new NotImplementedException(); + // Todo: This could probably be more efficient, likely at the cost of increased complexity. + // Number of "expired" or "used" items per player. + Dictionary perUserCounts = serverSidePlaylist + .GroupBy(item => item.OwnerID) + .ToDictionary(group => group.Key, group => group.Count(item => item.Expired)); + + // We'll run a simulation over all items which are not expired ("unprocessed"). Expired items will not have their ordering updated. + List processedItems = serverSidePlaylist.Where(item => item.Expired).ToList(); + List unprocessedItems = serverSidePlaylist.Where(item => !item.Expired).ToList(); + + // In every iteration of the simulation, pick the first available item from the user with the lowest number of items in the queue to add to the result set. + // If multiple users have the same number of items in the queue, then the item with the lowest ID is chosen. + while (unprocessedItems.Count > 0) + { + MultiplayerPlaylistItem candidateItem = unprocessedItems + .OrderBy(item => perUserCounts[item.OwnerID]) + .ThenBy(item => item.ID == 0 ? int.MaxValue : item.ID) + .First(); + + unprocessedItems.Remove(candidateItem); + processedItems.Add(candidateItem); + + perUserCounts[candidateItem.OwnerID]++; + } + + orderedItems = processedItems; + break; } - currentIndex = serverSidePlaylist.IndexOf(newItem); + for (int i = 0; i < orderedItems.Count; i++) + { + // Items which are already ordered correct don't need to be updated. + if (orderedItems[i].GameplayOrder == i) + continue; - long lastItem = room.Settings.PlaylistItemId; - room.Settings.PlaylistItemId = newItem.ID; + orderedItems[i].GameplayOrder = i; - if (notify && newItem.ID != lastItem) - await ((IMultiplayerClient)this).SettingsChanged(room.Settings).ConfigureAwait(false); + // Items which have an ID of 0 are not in the database, so avoid propagating database/hub events for them. + if (orderedItems[i].ID <= 0) + continue; + + await ((IMultiplayerClient)this).PlaylistItemChanged(orderedItems[i]).ConfigureAwait(false); + } + + serverSidePlaylist = orderedItems; } } } From 10932dd2824024418a24794c502f98f813cd4b3b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 2 Dec 2021 22:37:09 +0900 Subject: [PATCH 153/419] Remove now unnecessary test --- .../TestSceneMultiplayerQueueList.cs | 216 ------------------ 1 file changed, 216 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs deleted file mode 100644 index bd513ddf64..0000000000 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ /dev/null @@ -1,216 +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.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Testing; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Rooms; -using osu.Game.Rulesets.Osu; -using osu.Game.Screens.OnlinePlay; -using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; -using osu.Game.Tests.Beatmaps; - -namespace osu.Game.Tests.Visual.Multiplayer -{ - public class TestSceneMultiplayerQueueList : MultiplayerTestScene - { - private MultiplayerQueueList list; - private int currentItemId; - - [SetUp] - public new void Setup() => Schedule(() => - { - Child = list = new MultiplayerQueueList - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Width = 0.4f, - Height = 0.6f - }; - }); - - [SetUpSteps] - public override void SetUpSteps() - { - base.SetUpSteps(); - - // Not inside a step since this is used to affect steps added by the current test. - currentItemId = 0; - } - - [Test] - public void TestItemsAddedToEndInHostOnlyMode() - { - changeQueueModeStep(QueueMode.HostOnly); - - // User 1. - - PlaylistItem item1 = addItemStep(1); - assertPositionStep(item1, 0); - - PlaylistItem item2 = addItemStep(1); - assertPositionStep(item2, 1); - - // User 2. - - PlaylistItem item3 = addItemStep(2); - assertPositionStep(item3, 2); - } - - [Test] - public void TestItemsAddedToEndInAllPlayersMode() - { - changeQueueModeStep(QueueMode.AllPlayers); - - // User 1. - - PlaylistItem item1 = addItemStep(1); - assertPositionStep(item1, 0); - - PlaylistItem item2 = addItemStep(1); - assertPositionStep(item2, 1); - - // User 2. - - PlaylistItem item3 = addItemStep(2); - assertPositionStep(item3, 2); - } - - [Test] - public void TestItemsInsertedInCorrectPositionInRoundRobinMode() - { - changeQueueModeStep(QueueMode.AllPlayersRoundRobin); - - // User 1. - - PlaylistItem item1 = addItemStep(1); - assertPositionStep(item1, 0); - - PlaylistItem item2 = addItemStep(1); - assertPositionStep(item2, 1); - - // User 2. - - PlaylistItem item3 = addItemStep(2); - assertPositionStep(item3, 1); - assertPositionStep(item2, 2); - - PlaylistItem item4 = addItemStep(2); - assertPositionStep(item4, 3); - - PlaylistItem item5 = addItemStep(2); - assertPositionStep(item5, 4); - - // User 1. - - // This item is added to the end rather than injected between item4 and item5, since both users have an equal number - // of added items at this point and this user was the last of the two to add an item. - PlaylistItem item6 = addItemStep(1); - assertPositionStep(item6, 5); - - // User 3. - - PlaylistItem item7 = addItemStep(3); - assertPositionStep(item7, 2); - - PlaylistItem item8 = addItemStep(3); - assertPositionStep(item8, 5); - - PlaylistItem item9 = addItemStep(3); - assertPositionStep(item9, 8); - } - - [Test] - public void TestItemsReorderedWhenQueueModeChanged() - { - changeQueueModeStep(QueueMode.AllPlayers); - - var items = new List(); - - for (int i = 0; i < 8; i++) - items.Add(addItemStep(i <= 3 ? 1 : 2)); - - for (int i = 0; i < 8; i++) - assertPositionStep(items[i], i); - - changeQueueModeStep(QueueMode.AllPlayersRoundRobin); - - for (int i = 0; i < 4; i++) - { - assertPositionStep(items[i], i * 2); // Items by user 1. - assertPositionStep(items[i + 4], i * 2 + 1); // Items by user 2. - } - } - - [Test] - public void TestPreviouslyExpiredItemsConsideredInRoundRobinMode() - { - changeQueueModeStep(QueueMode.AllPlayersRoundRobin); - - // User 1. - - addItemStep(1, true); - addItemStep(1, true); - PlaylistItem item3 = addItemStep(1); - PlaylistItem item4 = addItemStep(1); - - // User2. - - PlaylistItem item5 = addItemStep(2); - PlaylistItem item6 = addItemStep(2); - - assertPositionStep(item5, 0); - assertPositionStep(item6, 1); - assertPositionStep(item3, 2); - assertPositionStep(item4, 3); - } - - /// - /// Adds a step to create a new playlist item. - /// - /// The item owner. - /// Whether the item should be added in an expired state. - /// The playlist item's ID. - private PlaylistItem addItemStep(int ownerId, bool expired = false) - { - var item = new PlaylistItem - { - ID = ++currentItemId, - OwnerID = ownerId, - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo, false).BeatmapInfo }, - Expired = expired - }; - - AddStep($"add {{ item: {item.ID}, user: {ownerId} }}", () => - { - SelectedRoom.Value.Playlist.Add(item); - if (!expired) - list.Items.Add(item); - }); - - return item; - } - - /// - /// Asserts the position of a given playlist item in the visual layout of the list. - /// - /// The playlist item. - /// The index at which the item should appear visually. The item with index 0 is at the top of the list. - private void assertPositionStep(PlaylistItem item, int visualIndex) - { - AddUntilStep($"item {item.ID} has pos = {visualIndex}", () => - { - return this.ChildrenOfType() - .OrderBy(drawable => drawable.Position.Y) - .TakeWhile(drawable => drawable.Item.ID != item.ID) - .Count() == visualIndex; - }); - } - - private void changeQueueModeStep(QueueMode newMode) => AddStep($"change queue mode to {newMode}", () => SelectedRoom.Value.QueueMode.Value = newMode); - } -} From abf7735b8403efb9e76e80b7d30ba49f4de8367d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 14:18:03 +0900 Subject: [PATCH 154/419] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index eff0eed278..cd2710c3ba 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7cc8893d8d..a4a4661822 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 9c21f76617..c095d3ef74 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From 614256697442c2c543d0120d1271be1264dfb858 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 14:26:53 +0900 Subject: [PATCH 155/419] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index cd2710c3ba..17b5cb67e9 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index a4a4661822..53a3337c9d 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index c095d3ef74..f3dc163a67 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + From 9803e63e6f0c9a2a14f6427c9faafb629c5ef2ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 14:30:15 +0900 Subject: [PATCH 156/419] Update IPC usage to return `null` --- osu.Game/IPC/ArchiveImportIPCChannel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/IPC/ArchiveImportIPCChannel.cs b/osu.Game/IPC/ArchiveImportIPCChannel.cs index d9d0e4c0ea..3ef1dc051d 100644 --- a/osu.Game/IPC/ArchiveImportIPCChannel.cs +++ b/osu.Game/IPC/ArchiveImportIPCChannel.cs @@ -25,6 +25,8 @@ namespace osu.Game.IPC { if (t.Exception != null) throw t.Exception; }, TaskContinuationOptions.OnlyOnFaulted); + + return null; }; } From 1d2d1bfcf3497c35a5e06b2eb3d322518d56ece2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 3 Dec 2021 14:38:14 +0900 Subject: [PATCH 157/419] Add UpdatedAt to MultiplayerPlaylistItem --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 5094ee510f..ec98de2b43 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -42,12 +42,16 @@ namespace osu.Game.Online.Rooms /// /// The order in which this will be played, starting from 0 and increasing for items which will be played later. /// - /// - /// Undefined value for items which are expired. - /// [Key(8)] public int GameplayOrder { get; set; } + /// + /// The date when this was last updated. + /// Not serialised to/from the client. + /// + [IgnoreMember] + public DateTimeOffset UpdatedAt { get; set; } + public MultiplayerPlaylistItem() { } From b75a5b778e0d22206dd17f0afce694825b39214f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 3 Dec 2021 15:05:52 +0900 Subject: [PATCH 158/419] Update history list to also sort by gameplay order --- .../Multiplayer/Match/Playlist/MultiplayerHistoryList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs index 76088180c4..25f658446a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist private class HistoryFillFlowContainer : FillFlowContainer> { - public override IEnumerable FlowingChildren => base.FlowingChildren.Reverse(); + public override IEnumerable FlowingChildren => base.FlowingChildren.OfType>().OrderByDescending(item => item.Model.GameplayOrder); } } } From 0a1304b92a00200c318828d995b63e1660ce1a18 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 3 Dec 2021 15:45:13 +0900 Subject: [PATCH 159/419] Remove gameplay_order, use existing playlist_order --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 2 +- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 4 ++-- osu.Game/Online/Rooms/PlaylistItem.cs | 4 ++-- .../Multiplayer/Match/Playlist/MultiplayerHistoryList.cs | 2 +- .../Multiplayer/Match/Playlist/MultiplayerQueueList.cs | 2 +- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 0ffb81d986..ae53ef2e52 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -742,7 +742,7 @@ namespace osu.Game.Online.Multiplayer Beatmap = { Value = apiBeatmap }, Ruleset = { Value = ruleset }, Expired = item.Expired, - GameplayOrder = item.GameplayOrder + PlaylistOrder = item.PlaylistOrder }; playlistItem.RequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))); diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index ec98de2b43..c84b5b9039 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -43,7 +43,7 @@ namespace osu.Game.Online.Rooms /// The order in which this will be played, starting from 0 and increasing for items which will be played later. /// [Key(8)] - public int GameplayOrder { get; set; } + public ushort PlaylistOrder { get; set; } /// /// The date when this was last updated. @@ -65,7 +65,7 @@ namespace osu.Game.Online.Rooms RequiredMods = item.RequiredMods.Select(m => new APIMod(m)).ToArray(); AllowedMods = item.AllowedMods.Select(m => new APIMod(m)).ToArray(); Expired = item.Expired; - GameplayOrder = item.GameplayOrder; + PlaylistOrder = item.PlaylistOrder ?? 0; } } } diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 4c7f9e139a..beefb17d54 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -33,8 +33,8 @@ namespace osu.Game.Online.Rooms [JsonProperty("expired")] public bool Expired { get; set; } - [JsonProperty("gameplay_order")] - public int GameplayOrder { get; set; } + [JsonProperty("playlist_order")] + public ushort? PlaylistOrder { get; set; } [JsonIgnore] public IBindable Valid => valid; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs index 25f658446a..ed41a6fc78 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist private class HistoryFillFlowContainer : FillFlowContainer> { - public override IEnumerable FlowingChildren => base.FlowingChildren.OfType>().OrderByDescending(item => item.Model.GameplayOrder); + public override IEnumerable FlowingChildren => base.FlowingChildren.OfType>().OrderByDescending(item => item.Model.PlaylistOrder); } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index 58f35b940a..1b1b66273f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist roomPlaylist.BindCollectionChanged((_, __) => InvalidateLayout()); } - public override IEnumerable FlowingChildren => base.FlowingChildren.OfType>().OrderBy(item => item.Model.GameplayOrder); + public override IEnumerable FlowingChildren => base.FlowingChildren.OfType>().OrderBy(item => item.Model.PlaylistOrder); } } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 2831c94429..b47b86a2d8 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -316,7 +316,7 @@ namespace osu.Game.Tests.Visual.Multiplayer case QueueMode.HostOnly: // In host-only mode, the current item is re-used. item.ID = currentItem.ID; - item.GameplayOrder = currentItem.GameplayOrder; + item.PlaylistOrder = currentItem.PlaylistOrder; serverSidePlaylist[currentIndex] = item; await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); @@ -488,10 +488,10 @@ namespace osu.Game.Tests.Visual.Multiplayer for (int i = 0; i < orderedItems.Count; i++) { // Items which are already ordered correct don't need to be updated. - if (orderedItems[i].GameplayOrder == i) + if (orderedItems[i].PlaylistOrder == i) continue; - orderedItems[i].GameplayOrder = i; + orderedItems[i].PlaylistOrder = (ushort)i; // Items which have an ID of 0 are not in the database, so avoid propagating database/hub events for them. if (orderedItems[i].ID <= 0) From 2262b7b331efdd1ecfaa8572687e03dacd175d8f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 15:35:06 +0900 Subject: [PATCH 160/419] Adjust logging to avoid using tabs --- osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs index 273e17d24b..a48a6fc631 100644 --- a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs +++ b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs @@ -36,8 +36,8 @@ namespace osu.Desktop.LegacyIpc { try { - logger.Add($"Processing legacy IPC message..."); - logger.Add($"\t{msg.Value}", LogLevel.Debug); + logger.Add("Processing legacy IPC message..."); + logger.Add($" {msg.Value}", LogLevel.Debug); var legacyData = ((JObject)msg.Value).ToObject(); object value = parseObject((JObject)legacyData!.MessageData, legacyData.MessageType); @@ -64,7 +64,7 @@ namespace osu.Desktop.LegacyIpc return value.ToObject(); default: - throw new ArgumentException($"Unknown type: {type}"); + throw new ArgumentException($"Unsupported object type {type}"); } } From 33992e11e09e0d8b8dc2ca851c15167abb3b0ed5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 15:40:53 +0900 Subject: [PATCH 161/419] Split out ruleset lookup code --- osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs index a48a6fc631..b7f8baf219 100644 --- a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs +++ b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs @@ -75,14 +75,7 @@ namespace osu.Desktop.LegacyIpc case LegacyIpcDifficultyCalculationRequest req: try { - Ruleset ruleset = req.RulesetId switch - { - 0 => new OsuRuleset(), - 1 => new TaikoRuleset(), - 2 => new CatchRuleset(), - 3 => new ManiaRuleset(), - _ => throw new ArgumentException("Invalid ruleset id") - }; + var ruleset = getLegacyRulesetFromID(req.RulesetId); Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray(); WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile, _ => ruleset); @@ -101,5 +94,26 @@ namespace osu.Desktop.LegacyIpc Console.WriteLine("Type not matched."); return null; } + + private static Ruleset getLegacyRulesetFromID(int rulesetId) + { + switch (rulesetId) + { + case 0: + return new OsuRuleset(); + + case 1: + return new TaikoRuleset(); + + case 2: + return new CatchRuleset(); + + case 3: + return new ManiaRuleset(); + + default: + throw new ArgumentException("Invalid ruleset id"); + } + } } } From 79d723172a30c44b432f852fb8d29e0742f521cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 15:48:40 +0900 Subject: [PATCH 162/419] Remove `Console.WriteLine` usage --- osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs index b7f8baf219..8ac946d4a4 100644 --- a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs +++ b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs @@ -89,10 +89,10 @@ namespace osu.Desktop.LegacyIpc { return new LegacyIpcDifficultyCalculationResponse(); } - } - Console.WriteLine("Type not matched."); - return null; + default: + throw new ArgumentException($"Unsupported message type {message}"); + } } private static Ruleset getLegacyRulesetFromID(int rulesetId) From f9ad3075261edfc9c590beda29a8f8323c18b197 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 15:49:01 +0900 Subject: [PATCH 163/419] Apply nullable --- osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs index 8ac946d4a4..2c9eb76dfc 100644 --- a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs +++ b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs @@ -15,6 +15,8 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Taiko; +#nullable enable + namespace osu.Desktop.LegacyIpc { /// @@ -27,7 +29,7 @@ namespace osu.Desktop.LegacyIpc /// /// Invoked when a message is received from a legacy client. /// - public new Func MessageReceived; + public new Func? MessageReceived; public LegacyTcpIpcProvider() : base(45357) @@ -42,8 +44,10 @@ namespace osu.Desktop.LegacyIpc var legacyData = ((JObject)msg.Value).ToObject(); object value = parseObject((JObject)legacyData!.MessageData, legacyData.MessageType); - object result = onLegacyIpcMessageReceived(value); - return result != null ? new LegacyIpcMessage { Value = result } : null; + return new LegacyIpcMessage + { + Value = onLegacyIpcMessageReceived(value) + }; } catch (Exception ex) { @@ -58,10 +62,12 @@ namespace osu.Desktop.LegacyIpc switch (type) { case nameof(LegacyIpcDifficultyCalculationRequest): - return value.ToObject(); + return value.ToObject() + ?? throw new InvalidOperationException($"Failed to parse request {value}"); case nameof(LegacyIpcDifficultyCalculationResponse): - return value.ToObject(); + return value.ToObject() + ?? throw new InvalidOperationException($"Failed to parse request {value}"); default: throw new ArgumentException($"Unsupported object type {type}"); From ba05a0a3834b6d02a07ef2f94e69a23b02584d14 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 16:04:11 +0900 Subject: [PATCH 164/419] Centralise specification of bracket.json filename --- .../NonVisual/CustomTourneyDirectoryTest.cs | 4 ++-- osu.Game.Tournament/IO/TournamentStorage.cs | 2 +- osu.Game.Tournament/TournamentGame.cs | 2 +- osu.Game.Tournament/TournamentGameBase.cs | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index d149ec145b..3619aae7e0 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -96,7 +96,7 @@ namespace osu.Game.Tournament.Tests.NonVisual Directory.CreateDirectory(flagsPath); // Define testing files corresponding to the specific file migrations that are needed - string bracketFile = Path.Combine(osuRoot, "bracket.json"); + string bracketFile = Path.Combine(osuRoot, TournamentGameBase.BRACKET_FILENAME); string drawingsConfig = Path.Combine(osuRoot, "drawings.ini"); string drawingsFile = Path.Combine(osuRoot, "drawings.txt"); @@ -133,7 +133,7 @@ namespace osu.Game.Tournament.Tests.NonVisual Assert.That(storage.GetFullPath("."), Is.EqualTo(migratedPath)); - Assert.True(storage.Exists("bracket.json")); + Assert.True(storage.Exists(TournamentGameBase.BRACKET_FILENAME)); Assert.True(storage.Exists("drawings.txt")); Assert.True(storage.Exists("drawings_results.txt")); diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 02cf567837..347d368a04 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -86,7 +86,7 @@ namespace osu.Game.Tournament.IO DeleteRecursive(source); } - moveFileIfExists("bracket.json", destination); + moveFileIfExists(TournamentGameBase.BRACKET_FILENAME, destination); moveFileIfExists("drawings.txt", destination); moveFileIfExists("drawings_results.txt", destination); moveFileIfExists("drawings.ini", destination); diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index f03f815b83..5d613894d4 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -71,7 +71,7 @@ namespace osu.Game.Tournament loadingSpinner.Expire(); Logger.Error(t.Exception, "Couldn't load bracket with error"); - Add(new WarningBox("Your bracket.json file could not be parsed. Please check runtime.log for more details.")); + Add(new WarningBox($"Your {BRACKET_FILENAME} file could not be parsed. Please check runtime.log for more details.")); }); return; diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index d2f146c4c2..9db007f3ee 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tournament [Cached(typeof(TournamentGameBase))] public class TournamentGameBase : OsuGameBase { - private const string bracket_filename = "bracket.json"; + public const string BRACKET_FILENAME = @"bracket.json"; private LadderInfo ladder; private TournamentStorage storage; private DependencyContainer dependencies; @@ -71,9 +71,9 @@ namespace osu.Game.Tournament { try { - if (storage.Exists(bracket_filename)) + if (storage.Exists(BRACKET_FILENAME)) { - using (Stream stream = storage.GetStream(bracket_filename, FileAccess.Read, FileMode.Open)) + using (Stream stream = storage.GetStream(BRACKET_FILENAME, FileAccess.Read, FileMode.Open)) using (var sr = new StreamReader(stream)) ladder = JsonConvert.DeserializeObject(sr.ReadToEnd(), new JsonPointConverter()); } @@ -309,7 +309,7 @@ namespace osu.Game.Tournament Converters = new JsonConverter[] { new JsonPointConverter() } }); - using (var stream = storage.GetStream(bracket_filename, FileAccess.Write, FileMode.Create)) + using (var stream = storage.GetStream(BRACKET_FILENAME, FileAccess.Write, FileMode.Create)) using (var sw = new StreamWriter(stream)) sw.Write(serialisedLadder); } From 5158736839ee93732531e2f596e2db22c0076bbc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 16:06:38 +0900 Subject: [PATCH 165/419] Avoid saving bracket if parsing failed, at all costs --- osu.Game.Tournament/TournamentGameBase.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 9db007f3ee..d08322a3e8 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.IO.Stores; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Graphics; using osu.Game.Online.API.Requests; @@ -32,9 +33,9 @@ namespace osu.Game.Tournament private DependencyContainer dependencies; private FileBasedIPC ipc; - protected Task BracketLoadTask => taskCompletionSource.Task; + protected Task BracketLoadTask => bracketLoadTaskCompletionSource.Task; - private readonly TaskCompletionSource taskCompletionSource = new TaskCompletionSource(); + private readonly TaskCompletionSource bracketLoadTaskCompletionSource = new TaskCompletionSource(); protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { @@ -144,7 +145,7 @@ namespace osu.Game.Tournament } catch (Exception e) { - taskCompletionSource.SetException(e); + bracketLoadTaskCompletionSource.SetException(e); return; } @@ -156,7 +157,7 @@ namespace osu.Game.Tournament dependencies.CacheAs(ipc = new FileBasedIPC()); Add(ipc); - taskCompletionSource.SetResult(true); + bracketLoadTaskCompletionSource.SetResult(true); initialisationText.Expire(); }); @@ -292,6 +293,12 @@ namespace osu.Game.Tournament protected virtual void SaveChanges() { + if (!bracketLoadTaskCompletionSource.Task.IsCompletedSuccessfully) + { + Logger.Log("Inhibiting bracket save as bracket parsing failed"); + return; + } + foreach (var r in ladder.Rounds) r.Matches = ladder.Matches.Where(p => p.Round.Value == r).Select(p => p.ID).ToList(); From dad5b06e8402205a67e6b4d5b899e2de642e6aef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 16:23:39 +0900 Subject: [PATCH 166/419] Avoid sending empty parameters in `GetBeatmapRequest` --- osu.Game/Online/API/Requests/GetBeatmapRequest.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/API/Requests/GetBeatmapRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs index 6cd45a41df..671f543422 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs @@ -26,9 +26,12 @@ namespace osu.Game.Online.API.Requests { var request = base.CreateWebRequest(); - request.AddParameter(@"id", beatmapInfo.OnlineID.ToString()); - request.AddParameter(@"checksum", beatmapInfo.MD5Hash); - request.AddParameter(@"filename", filename); + if (beatmapInfo.OnlineID > 0) + request.AddParameter(@"id", beatmapInfo.OnlineID.ToString()); + if (!string.IsNullOrEmpty(beatmapInfo.MD5Hash)) + request.AddParameter(@"checksum", beatmapInfo.MD5Hash); + if (!string.IsNullOrEmpty(filename)) + request.AddParameter(@"filename", filename); return request; } From e4b296e16ee7b144e0529c7890328c14b7eb5389 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 16:36:27 +0900 Subject: [PATCH 167/419] Use `OptIn` serialisation on `SkinInfo` to avoid writing unnecessary information --- osu.Game/Skinning/SkinInfo.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index 9a82964933..20c66a23a6 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Newtonsoft.Json; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Testing; using osu.Game.Database; @@ -16,6 +17,7 @@ namespace osu.Game.Skinning { [ExcludeFromDynamicCompile] [MapTo("Skin")] + [JsonObject(MemberSerialization.OptIn)] public class SkinInfo : RealmObject, IHasRealmFiles, IEquatable, IHasGuidPrimaryKey, ISoftDelete, IHasNamedFiles { internal static readonly Guid DEFAULT_SKIN = new Guid("2991CFD8-2140-469A-BCB9-2EC23FBCE4AD"); @@ -23,18 +25,22 @@ namespace osu.Game.Skinning internal static readonly Guid RANDOM_SKIN = new Guid("D39DFEFB-477C-4372-B1EA-2BCEA5FB8908"); [PrimaryKey] + [JsonProperty] public Guid ID { get; set; } = Guid.NewGuid(); + [JsonProperty] public string Name { get; set; } = string.Empty; + [JsonProperty] public string Creator { get; set; } = string.Empty; + [JsonProperty] + public string InstantiationInfo { get; set; } = string.Empty; + public string Hash { get; set; } = string.Empty; public bool Protected { get; set; } - public string InstantiationInfo { get; set; } = string.Empty; - public virtual Skin CreateInstance(IStorageResourceProvider resources) { var type = string.IsNullOrEmpty(InstantiationInfo) From 487a71312e3cebbea75902dbae6146ee0ef79ee3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 3 Dec 2021 16:40:20 +0900 Subject: [PATCH 168/419] Split out code so base methods aren't called --- .../Match/Playlist/MultiplayerPlaylist.cs | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index 2c50c88de8..c3245b550f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist if (firstPopulation) { foreach (var item in Room.Playlist) - PlaylistItemAdded(item); + addItemToLists(item); firstPopulation = false; } @@ -95,7 +95,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist protected override void PlaylistItemAdded(MultiplayerPlaylistItem item) { base.PlaylistItemAdded(item); + addItemToLists(item); + } + protected override void PlaylistItemRemoved(long item) + { + base.PlaylistItemRemoved(item); + removeItemFromLists(item); + } + + protected override void PlaylistItemChanged(MultiplayerPlaylistItem item) + { + base.PlaylistItemChanged(item); + + removeItemFromLists(item.ID); + addItemToLists(item); + } + + private void addItemToLists(MultiplayerPlaylistItem item) + { var apiItem = Playlist.Single(i => i.ID == item.ID); if (item.Expired) @@ -104,20 +122,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist queueList.Items.Add(apiItem); } - protected override void PlaylistItemRemoved(long item) + private void removeItemFromLists(long item) { - base.PlaylistItemRemoved(item); - queueList.Items.RemoveAll(i => i.ID == item); historyList.Items.RemoveAll(i => i.ID == item); } - - protected override void PlaylistItemChanged(MultiplayerPlaylistItem item) - { - base.PlaylistItemChanged(item); - - PlaylistItemRemoved(item.ID); - PlaylistItemAdded(item); - } } } From 9d6fe558c2d554e40203e963eceb72c619a7e5c7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 3 Dec 2021 16:48:54 +0900 Subject: [PATCH 169/419] Update TestMultiplayerClient with expired item ordering --- .../Multiplayer/TestMultiplayerClient.cs | 57 ++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index b47b86a2d8..f2d5323386 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -45,11 +45,13 @@ namespace osu.Game.Tests.Visual.Multiplayer /// /// Guaranteed up-to-date playlist. /// - private List serverSidePlaylist = new List(); + private readonly List serverSidePlaylist = new List(); private MultiplayerPlaylistItem? currentItem => Room?.Playlist[currentIndex]; private int currentIndex; + private long lastPlaylistItemId; + public TestMultiplayerClient(TestMultiplayerRoomManager roomManager) { this.roomManager = roomManager; @@ -169,6 +171,7 @@ namespace osu.Game.Tests.Visual.Multiplayer serverSidePlaylist.Clear(); serverSidePlaylist.AddRange(apiRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item))); + lastPlaylistItemId = serverSidePlaylist.Max(item => item.ID); var localUser = new MultiplayerRoomUser(api.LocalUser.Value.Id) { @@ -397,10 +400,13 @@ namespace osu.Game.Tests.Visual.Multiplayer // Expire the current playlist item. currentItem.Expired = true; + currentItem.UpdatedAt = DateTimeOffset.Now; + await ((IMultiplayerClient)this).PlaylistItemChanged(currentItem).ConfigureAwait(false); + await updatePlaylistOrder(Room).ConfigureAwait(false); // In host-only mode, a duplicate playlist item will be used for the next round. - if (Room.Settings.QueueMode == QueueMode.HostOnly) + if (Room.Settings.QueueMode == QueueMode.HostOnly && serverSidePlaylist.All(item => item.Expired)) await duplicateCurrentItem().ConfigureAwait(false); await updateCurrentItem(Room).ConfigureAwait(false); @@ -424,11 +430,14 @@ namespace osu.Game.Tests.Visual.Multiplayer { Debug.Assert(Room != null); + // Some tests can add items in already-expired states. + item.UpdatedAt = DateTimeOffset.Now; + // Add the item to the list first in order to compute gameplay order. serverSidePlaylist.Add(item); await updatePlaylistOrder(Room).ConfigureAwait(false); - item.ID = serverSidePlaylist[^2].ID + 1; + item.ID = ++lastPlaylistItemId; await ((IMultiplayerClient)this).PlaylistItemAdded(item).ConfigureAwait(false); } @@ -447,15 +456,17 @@ namespace osu.Game.Tests.Visual.Multiplayer private async Task updatePlaylistOrder(MultiplayerRoom room) { - List orderedItems; + List orderedActiveItems; switch (room.Settings.QueueMode) { default: - orderedItems = serverSidePlaylist.OrderBy(item => item.ID == 0 ? int.MaxValue : item.ID).ToList(); + orderedActiveItems = serverSidePlaylist.Where(item => !item.Expired).OrderBy(item => item.ID == 0 ? int.MaxValue : item.ID).ToList(); break; case QueueMode.AllPlayersRoundRobin: + orderedActiveItems = new List(); + // Todo: This could probably be more efficient, likely at the cost of increased complexity. // Number of "expired" or "used" items per player. Dictionary perUserCounts = serverSidePlaylist @@ -463,7 +474,6 @@ namespace osu.Game.Tests.Visual.Multiplayer .ToDictionary(group => group.Key, group => group.Count(item => item.Expired)); // We'll run a simulation over all items which are not expired ("unprocessed"). Expired items will not have their ordering updated. - List processedItems = serverSidePlaylist.Where(item => item.Expired).ToList(); List unprocessedItems = serverSidePlaylist.Where(item => !item.Expired).ToList(); // In every iteration of the simulation, pick the first available item from the user with the lowest number of items in the queue to add to the result set. @@ -476,31 +486,40 @@ namespace osu.Game.Tests.Visual.Multiplayer .First(); unprocessedItems.Remove(candidateItem); - processedItems.Add(candidateItem); + orderedActiveItems.Add(candidateItem); perUserCounts[candidateItem.OwnerID]++; } - orderedItems = processedItems; break; } - for (int i = 0; i < orderedItems.Count; i++) - { - // Items which are already ordered correct don't need to be updated. - if (orderedItems[i].PlaylistOrder == i) - continue; + // For expired items, it's important that they're ordered in ascending order such that the last updated item is the last in the list. + // This is so that the updated_at database column doesn't get refreshed as a result of change in ordering. + List orderedExpiredItems = serverSidePlaylist.Where(item => item.Expired).OrderBy(item => item.UpdatedAt).ToList(); + for (int i = 0; i < orderedExpiredItems.Count; i++) + await setOrder(orderedExpiredItems[i], (ushort)i).ConfigureAwait(false); - orderedItems[i].PlaylistOrder = (ushort)i; + for (int i = 0; i < orderedActiveItems.Count; i++) + await setOrder(orderedActiveItems[i], (ushort)i).ConfigureAwait(false); + + serverSidePlaylist.Clear(); + serverSidePlaylist.AddRange(orderedExpiredItems); + serverSidePlaylist.AddRange(orderedActiveItems); + + async Task setOrder(MultiplayerPlaylistItem item, ushort order) + { + if (item.PlaylistOrder == order) + return; + + item.PlaylistOrder = order; // Items which have an ID of 0 are not in the database, so avoid propagating database/hub events for them. - if (orderedItems[i].ID <= 0) - continue; + if (item.ID <= 0) + return; - await ((IMultiplayerClient)this).PlaylistItemChanged(orderedItems[i]).ConfigureAwait(false); + await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); } - - serverSidePlaylist = orderedItems; } } } From 2927b235dec25bc6c27cac9786e1d0736ccea593 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 17:18:02 +0900 Subject: [PATCH 170/419] Add test coverage of mouse wheel scroll adjusting volume --- .../TestSceneMouseWheelVolumeAdjust.cs | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs b/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs new file mode 100644 index 0000000000..9e684e4f10 --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs @@ -0,0 +1,99 @@ +// 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.Configuration; +using osu.Game.Screens.Play; +using osu.Game.Tests.Beatmaps.IO; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Navigation +{ + public class TestSceneMouseWheelVolumeAdjust : OsuGameTestScene + { + public override void SetUpSteps() + { + base.SetUpSteps(); + + // Headless tests are always at minimum volume. This covers interactive tests, matching that initial value. + AddStep("Set volume to min", () => Game.Audio.Volume.Value = 0); + AddAssert("Volume is min", () => Game.Audio.AggregateVolume.Value == 0); + AddStep("Move mouse to centre", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre)); + } + + [Test] + public void TestAdjustVolumeFromMainMenu() + { + // First scroll makes volume controls appear, second adjusts volume. + AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 2); + AddUntilStep("Volume is above zero", () => Game.Audio.AggregateVolume.Value > 0); + } + + [Test] + public void TestAdjustVolumeFromPlayerWheelEnabled() + { + loadToPlayerNonBreakTime(); + + // First scroll makes volume controls appear, second adjusts volume. + AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 2); + AddAssert("Volume is above zero", () => Game.Audio.Volume.Value > 0); + } + + [Test] + public void TestAdjustVolumeFromPlayerWheelDisabled() + { + AddStep("disable wheel volume adjust", () => Game.LocalConfig.SetValue(OsuSetting.MouseDisableWheel, true)); + + loadToPlayerNonBreakTime(); + + // First scroll makes volume controls appear, second adjusts volume. + AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 2); + AddAssert("Volume is still zero", () => Game.Audio.Volume.Value == 0); + } + + [Test] + public void TestAdjustVolumeFromPlayerWheelDisabledHoldingAlt() + { + AddStep("disable wheel volume adjust", () => Game.LocalConfig.SetValue(OsuSetting.MouseDisableWheel, true)); + + loadToPlayerNonBreakTime(); + + // First scroll makes volume controls appear, second adjusts volume. + AddRepeatStep("Adjust volume using mouse wheel holding alt", () => + { + InputManager.PressKey(Key.AltLeft); + InputManager.ScrollVerticalBy(5); + InputManager.ReleaseKey(Key.AltLeft); + }, 2); + + AddAssert("Volume is above zero", () => Game.Audio.Volume.Value > 0); + } + + private void loadToPlayerNonBreakTime() + { + Player player = null; + Screens.Select.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new TestSceneScreenNavigation.TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + + AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).Wait()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => + { + // dismiss any notifications that may appear (ie. muted notification). + clickMouseInCentre(); + return (player = Game.ScreenStack.CurrentScreen as Player) != null; + }); + + AddUntilStep("wait for play time active", () => !player.IsBreakTime.Value); + } + + private void clickMouseInCentre() + { + InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre); + InputManager.Click(MouseButton.Left); + } + } +} From aaa46960b3967562034481e9773f6d2d2af51893 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 16:56:14 +0900 Subject: [PATCH 171/419] Reword mouse wheel disable setting to better explain its purpose --- osu.Game/Localisation/MouseSettingsStrings.cs | 9 +++++++-- .../Overlays/Settings/Sections/Input/MouseSettings.cs | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Localisation/MouseSettingsStrings.cs b/osu.Game/Localisation/MouseSettingsStrings.cs index 5e894c4e0b..fd7225ad2e 100644 --- a/osu.Game/Localisation/MouseSettingsStrings.cs +++ b/osu.Game/Localisation/MouseSettingsStrings.cs @@ -35,9 +35,14 @@ namespace osu.Game.Localisation public static LocalisableString ConfineMouseMode => new TranslatableString(getKey(@"confine_mouse_mode"), @"Confine mouse cursor to window"); /// - /// "Disable mouse wheel during gameplay" + /// "Disable mouse wheel adjusting volume during gameplay" /// - public static LocalisableString DisableMouseWheel => new TranslatableString(getKey(@"disable_mouse_wheel"), @"Disable mouse wheel during gameplay"); + public static LocalisableString DisableMouseWheelVolumeAdjust => new TranslatableString(getKey(@"disable_mouse_wheel_volume_adjust"), @"Disable mouse wheel adjusting volume during gameplay"); + + /// + /// "Volume can still be adjusted using the mouse wheel by holding "Alt"" + /// + public static LocalisableString DisableMouseWheelVolumeAdjustTooltip => new TranslatableString(getKey(@"disable_mouse_wheel_volume_adjust_tooltip"), @"Volume can still be adjusted using the mouse wheel by holding ""Alt"""); /// /// "Disable mouse buttons during gameplay" diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index 0334167759..4235dc0a05 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -67,7 +67,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input }, new SettingsCheckbox { - LabelText = MouseSettingsStrings.DisableMouseWheel, + LabelText = MouseSettingsStrings.DisableMouseWheelVolumeAdjust, + TooltipText = MouseSettingsStrings.DisableMouseWheelVolumeAdjustTooltip, Current = osuConfig.GetBindable(OsuSetting.MouseDisableWheel) }, new SettingsCheckbox From 6b7367240354715ade59470e1488e90ba3772cc7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 16:56:34 +0900 Subject: [PATCH 172/419] Stop `Player` from blocking volume adjust when `Alt` it held Similar case to what we already have in `OsuScrollContainer`, so there is precedent for handling this locally in this fashion. --- osu.Game/Screens/Play/Player.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 4d574dea99..a0e9428cff 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -768,7 +768,15 @@ namespace osu.Game.Screens.Play Scheduler.Add(resultsDisplayDelegate); } - protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; + protected override bool OnScroll(ScrollEvent e) + { + // During pause, allow global volume adjust regardless of settings. + if (GameplayClockContainer.IsPaused.Value) + return false; + + // Block global volume adjust if the user has asked for it (special case when holding "Alt"). + return mouseWheelDisabled.Value && !e.AltPressed; + } #region Fail Logic From 675ecb603f7500dd3b92547b703e4e14b985be3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 17:50:07 +0900 Subject: [PATCH 173/419] Add `IRulesetStore` to allow for transitional usage in upcoming manager classes --- osu.Game.Tests/Database/RulesetStoreTests.cs | 6 ++-- osu.Game/Rulesets/IRulesetStore.cs | 31 ++++++++++++++++++++ osu.Game/Rulesets/RulesetStore.cs | 10 ++++++- osu.Game/Stores/RealmRulesetStore.cs | 18 ++++++++---- 4 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 osu.Game/Rulesets/IRulesetStore.cs diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index f4e0838be1..b82dab1874 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -45,9 +45,9 @@ namespace osu.Game.Tests.Database { var rulesets = new RealmRulesetStore(realmFactory, storage); - Assert.IsTrue((rulesets.AvailableRulesets.First() as RealmRuleset)?.IsManaged == false); - Assert.IsTrue((rulesets.GetRuleset(0) as RealmRuleset)?.IsManaged == false); - Assert.IsTrue((rulesets.GetRuleset("mania") as RealmRuleset)?.IsManaged == false); + Assert.IsTrue(rulesets.AvailableRulesets.First().IsManaged == false); + Assert.IsTrue(rulesets.GetRuleset(0)?.IsManaged == false); + Assert.IsTrue(rulesets.GetRuleset("mania")?.IsManaged == false); }); } } diff --git a/osu.Game/Rulesets/IRulesetStore.cs b/osu.Game/Rulesets/IRulesetStore.cs new file mode 100644 index 0000000000..08d907810b --- /dev/null +++ b/osu.Game/Rulesets/IRulesetStore.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; + +#nullable enable + +namespace osu.Game.Rulesets +{ + public interface IRulesetStore + { + /// + /// Retrieve a ruleset using a known ID. + /// + /// The ruleset's internal ID. + /// A ruleset, if available, else null. + IRulesetInfo? GetRuleset(int id); + + /// + /// Retrieve a ruleset using a known short name. + /// + /// The ruleset's short name. + /// A ruleset, if available, else null. + IRulesetInfo? GetRuleset(string shortName); + + /// + /// All available rulesets. + /// + IEnumerable AvailableRulesets { get; } + } +} diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 6dd036c0e6..5cc6a75f43 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -13,7 +13,7 @@ using osu.Game.Database; namespace osu.Game.Rulesets { - public class RulesetStore : DatabaseBackedStore, IDisposable + public class RulesetStore : DatabaseBackedStore, IRulesetStore, IDisposable { private const string ruleset_library_prefix = "osu.Game.Rulesets"; @@ -236,5 +236,13 @@ namespace osu.Game.Rulesets { AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetDependencyAssembly; } + + #region Implementation of IRulesetStore + + IRulesetInfo IRulesetStore.GetRuleset(int id) => GetRuleset(id); + IRulesetInfo IRulesetStore.GetRuleset(string shortName) => GetRuleset(shortName); + IEnumerable IRulesetStore.AvailableRulesets => AvailableRulesets; + + #endregion } } diff --git a/osu.Game/Stores/RealmRulesetStore.cs b/osu.Game/Stores/RealmRulesetStore.cs index 0119aec9a4..93b6d29e7d 100644 --- a/osu.Game/Stores/RealmRulesetStore.cs +++ b/osu.Game/Stores/RealmRulesetStore.cs @@ -18,7 +18,7 @@ using osu.Game.Rulesets; namespace osu.Game.Stores { - public class RealmRulesetStore : IDisposable + public class RealmRulesetStore : IRulesetStore, IDisposable { private readonly RealmContextFactory realmFactory; @@ -29,9 +29,9 @@ namespace osu.Game.Stores /// /// All available rulesets. /// - public IEnumerable AvailableRulesets => availableRulesets; + public IEnumerable AvailableRulesets => availableRulesets; - private readonly List availableRulesets = new List(); + private readonly List availableRulesets = new List(); public RealmRulesetStore(RealmContextFactory realmFactory, Storage? storage = null) { @@ -64,14 +64,14 @@ namespace osu.Game.Stores /// /// The ruleset's internal ID. /// A ruleset, if available, else null. - public IRulesetInfo? GetRuleset(int id) => AvailableRulesets.FirstOrDefault(r => r.OnlineID == id); + public RealmRuleset? GetRuleset(int id) => AvailableRulesets.FirstOrDefault(r => r.OnlineID == id); /// /// Retrieve a ruleset using a known short name. /// /// The ruleset's short name. /// A ruleset, if available, else null. - public IRulesetInfo? GetRuleset(string shortName) => AvailableRulesets.FirstOrDefault(r => r.ShortName == shortName); + public RealmRuleset? GetRuleset(string shortName) => AvailableRulesets.FirstOrDefault(r => r.ShortName == shortName); private Assembly? resolveRulesetDependencyAssembly(object? sender, ResolveEventArgs args) { @@ -258,5 +258,13 @@ namespace osu.Game.Stores { AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetDependencyAssembly; } + + #region Implementation of IRulesetStore + + IRulesetInfo? IRulesetStore.GetRuleset(int id) => GetRuleset(id); + IRulesetInfo? IRulesetStore.GetRuleset(string shortName) => GetRuleset(shortName); + IEnumerable IRulesetStore.AvailableRulesets => AvailableRulesets; + + #endregion } } From 15db1372aafa23a0b431fdf54465f1b7fdf55d18 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 18:01:29 +0900 Subject: [PATCH 174/419] Add missing equality implementations on `IRulesetInfo` --- osu.Game/Models/RealmRuleset.cs | 2 ++ osu.Game/Rulesets/IRulesetInfo.cs | 3 ++- osu.Game/Rulesets/RulesetInfo.cs | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Models/RealmRuleset.cs b/osu.Game/Models/RealmRuleset.cs index 9a7488fda2..b959d0b4dc 100644 --- a/osu.Game/Models/RealmRuleset.cs +++ b/osu.Game/Models/RealmRuleset.cs @@ -50,6 +50,8 @@ namespace osu.Game.Models public bool Equals(RealmRuleset? other) => other != null && OnlineID == other.OnlineID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; + public bool Equals(IRulesetInfo? other) => other is RealmRuleset b && Equals(b); + public override string ToString() => Name; public RealmRuleset Clone() => new RealmRuleset diff --git a/osu.Game/Rulesets/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs index 4e529a73fb..6599e0d59d 100644 --- a/osu.Game/Rulesets/IRulesetInfo.cs +++ b/osu.Game/Rulesets/IRulesetInfo.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Database; #nullable enable @@ -10,7 +11,7 @@ namespace osu.Game.Rulesets /// /// A representation of a ruleset's metadata. /// - public interface IRulesetInfo : IHasOnlineID + public interface IRulesetInfo : IHasOnlineID, IEquatable { /// /// The user-exposed name of this ruleset. diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 4a146c05bf..d018cc4194 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -49,6 +49,8 @@ namespace osu.Game.Rulesets public override bool Equals(object obj) => obj is RulesetInfo rulesetInfo && Equals(rulesetInfo); + public bool Equals(IRulesetInfo other) => other is RulesetInfo b && Equals(b); + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] public override int GetHashCode() { From e75e209053adc92ba696110b87b5691a1aa0b9c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 18:14:44 +0900 Subject: [PATCH 175/419] Cache and consume `IRulesetStore` where feasible --- osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs | 2 +- .../Visual/Online/TestSceneBeatmapRulesetSelector.cs | 2 +- .../Visual/Online/TestSceneBeatmapSetOverlay.cs | 2 +- osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs | 2 +- .../Ranking/TestSceneContractedPanelMiddleContent.cs | 2 +- .../Components/TestSceneTournamentModDisplay.cs | 2 +- osu.Game.Tournament/Components/TournamentModIcon.cs | 2 +- osu.Game.Tournament/IPC/FileBasedIPC.cs | 2 +- osu.Game/Beatmaps/DifficultyRecommender.cs | 6 +++--- osu.Game/Beatmaps/Drawables/DifficultyIcon.cs | 2 +- osu.Game/Online/API/Requests/GetUserRequest.cs | 6 +++--- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 5 ++++- osu.Game/Online/Rooms/PlaylistItem.cs | 5 ++++- osu.Game/OsuGameBase.cs | 1 + osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs | 9 +++++++-- .../Profile/Sections/Recent/DrawableRecentActivity.cs | 2 +- osu.Game/Screens/OnlinePlay/Components/RoomManager.cs | 2 +- .../Multiplayer/Participants/ParticipantPanel.cs | 4 ++-- osu.Game/Users/UserActivity.cs | 10 +++++----- 19 files changed, 40 insertions(+), 28 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index 57d60cea9e..c65595d82e 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Menus private TestToolbar toolbar; [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } [SetUp] public void SetUp() => Schedule(() => diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs index 90f3eb64e4..63741451f3 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Online } [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } [Test] public void TestMultipleRulesetsBeatmapSet() diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index 3314e291e8..f87cca80b0 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Online } [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } [Test] public void TestLoading() diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 19e06beaad..52d5eb2c65 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Online private TestUserListPanel evast; [Resolved] - private RulesetStore rulesetStore { get; set; } + private IRulesetStore rulesetStore { get; set; } [SetUp] public void SetUp() => Schedule(() => diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs index acacdf8644..9ed135e8b8 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Ranking public class TestSceneContractedPanelMiddleContent : OsuTestScene { [Resolved] - private RulesetStore rulesetStore { get; set; } + private IRulesetStore rulesetStore { get; set; } [Test] public void TestShowPanel() diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs index 3cd13df0d3..9feef36a02 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tournament.Tests.Components private IAPIProvider api { get; set; } [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } private FillFlowContainer fillFlow; diff --git a/osu.Game.Tournament/Components/TournamentModIcon.cs b/osu.Game.Tournament/Components/TournamentModIcon.cs index 0fde263bc8..ed8a36c220 100644 --- a/osu.Game.Tournament/Components/TournamentModIcon.cs +++ b/osu.Game.Tournament/Components/TournamentModIcon.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tournament.Components private readonly string modAcronym; [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } public TournamentModIcon(string modAcronym) { diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index a57f9fd691..5278d538d2 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -28,7 +28,7 @@ namespace osu.Game.Tournament.IPC protected IAPIProvider API { get; private set; } [Resolved] - protected RulesetStore Rulesets { get; private set; } + protected IRulesetStore Rulesets { get; private set; } [Resolved] private GameHost host { get; set; } diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index 8b00d0f7f2..3949e84f4a 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps private IAPIProvider api { get; set; } [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } [Resolved] private Bindable ruleset { get; set; } @@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps /// private int? requestedUserId; - private readonly Dictionary recommendedDifficultyMapping = new Dictionary(); + private readonly Dictionary recommendedDifficultyMapping = new Dictionary(); private readonly IBindable apiState = new Bindable(); @@ -101,7 +101,7 @@ namespace osu.Game.Beatmaps /// Rulesets ordered descending by their respective recommended difficulties. /// The currently selected ruleset will always be first. /// - private IEnumerable orderedRulesets + private IEnumerable orderedRulesets { get { diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 6e573cc2a0..82be0559a7 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -86,7 +86,7 @@ namespace osu.Game.Beatmaps.Drawables } [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } [BackgroundDependencyLoader] private void load(OsuColour colours) diff --git a/osu.Game/Online/API/Requests/GetUserRequest.cs b/osu.Game/Online/API/Requests/GetUserRequest.cs index e32451fc2f..28da5222f9 100644 --- a/osu.Game/Online/API/Requests/GetUserRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserRequest.cs @@ -9,7 +9,7 @@ namespace osu.Game.Online.API.Requests public class GetUserRequest : APIRequest { public readonly string Lookup; - public readonly RulesetInfo Ruleset; + public readonly IRulesetInfo Ruleset; private readonly LookupType lookupType; /// @@ -24,7 +24,7 @@ namespace osu.Game.Online.API.Requests /// /// The user to get. /// The ruleset to get the user's info for. - public GetUserRequest(long? userId = null, RulesetInfo ruleset = null) + public GetUserRequest(long? userId = null, IRulesetInfo ruleset = null) { Lookup = userId.ToString(); lookupType = LookupType.Id; @@ -36,7 +36,7 @@ namespace osu.Game.Online.API.Requests /// /// The user to get. /// The ruleset to get the user's info for. - public GetUserRequest(string username = null, RulesetInfo ruleset = null) + public GetUserRequest(string username = null, IRulesetInfo ruleset = null) { Lookup = username; lookupType = LookupType.Username; diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 0822c29376..19ee6a3af3 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -94,7 +94,7 @@ namespace osu.Game.Online.Multiplayer protected IAPIProvider API { get; private set; } = null!; [Resolved] - protected RulesetStore Rulesets { get; private set; } = null!; + protected IRulesetStore Rulesets { get; private set; } = null!; [Resolved] private UserLookupCache userLookupCache { get; set; } = null!; @@ -706,6 +706,9 @@ namespace osu.Game.Online.Multiplayer var apiBeatmap = await GetAPIBeatmap(item.BeatmapID).ConfigureAwait(false); var ruleset = Rulesets.GetRuleset(item.RulesetID); + + Debug.Assert(ruleset != null); + var rulesetInstance = ruleset.CreateInstance(); var playlistItem = new PlaylistItem diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index a1480865b8..3eae03c702 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.Linq; using Newtonsoft.Json; using osu.Framework.Bindables; @@ -79,11 +80,13 @@ namespace osu.Game.Online.Rooms public void MarkInvalid() => valid.Value = false; - public void MapObjects(RulesetStore rulesets) + public void MapObjects(IRulesetStore rulesets) { Beatmap.Value ??= apiBeatmap; Ruleset.Value ??= rulesets.GetRuleset(RulesetID); + Debug.Assert(Ruleset.Value != null); + Ruleset rulesetInstance = Ruleset.Value.CreateInstance(); if (allowedModsBacking != null) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 34344f8022..e852e3955b 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -196,6 +196,7 @@ namespace osu.Game runMigrations(); dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage)); + dependencies.CacheAs(RulesetStore); dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", contextFactory)); diff --git a/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs b/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs index a9723c9c62..25aed4c980 100644 --- a/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs +++ b/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs @@ -47,7 +47,7 @@ namespace osu.Game.Overlays.BeatmapSet } [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } private void onRulesetChanged(ValueChangedEvent ruleset) { @@ -57,8 +57,13 @@ namespace osu.Game.Overlays.BeatmapSet if (ruleset.NewValue == null) return; + var rulesetInstance = rulesets.GetRuleset(ruleset.NewValue.OnlineID)?.CreateInstance(); + + if (rulesetInstance == null) + return; + modsContainer.Add(new ModButton(new ModNoMod())); - modsContainer.AddRange(rulesets.GetRuleset(ruleset.NewValue.OnlineID).CreateInstance().AllMods.Where(m => m.UserPlayable).Select(m => new ModButton(m))); + modsContainer.AddRange(rulesetInstance.AllMods.Where(m => m.UserPlayable).Select(m => new ModButton(m))); modsContainer.ForEach(button => { diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs index cb8dae0bbc..7a27c6e4e1 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent private IAPIProvider api { get; set; } [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } private readonly APIRecentActivity activity; diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index ddfdab18f7..1391422f9f 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Components private readonly Bindable joinedRoom = new Bindable(); [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } [Resolved] private BeatmapManager beatmaps { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 3152f50d3d..a68309dae5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -35,7 +35,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants private IAPIProvider api { get; set; } [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } private SpriteIcon crown; @@ -185,7 +185,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; // Todo: Should use the room's selected item to determine ruleset. - var ruleset = rulesets.GetRuleset(0).CreateInstance(); + var ruleset = rulesets.GetRuleset(0)?.CreateInstance(); int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank; userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 0874685f49..516aa80652 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -29,9 +29,9 @@ namespace osu.Game.Users { public IBeatmapInfo BeatmapInfo { get; } - public RulesetInfo Ruleset { get; } + public IRulesetInfo Ruleset { get; } - protected InGame(IBeatmapInfo beatmapInfo, RulesetInfo ruleset) + protected InGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) { BeatmapInfo = beatmapInfo; Ruleset = ruleset; @@ -42,7 +42,7 @@ namespace osu.Game.Users public class InMultiplayerGame : InGame { - public InMultiplayerGame(IBeatmapInfo beatmapInfo, RulesetInfo ruleset) + public InMultiplayerGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) : base(beatmapInfo, ruleset) { } @@ -52,7 +52,7 @@ namespace osu.Game.Users public class InPlaylistGame : InGame { - public InPlaylistGame(IBeatmapInfo beatmapInfo, RulesetInfo ruleset) + public InPlaylistGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) : base(beatmapInfo, ruleset) { } @@ -60,7 +60,7 @@ namespace osu.Game.Users public class InSoloGame : InGame { - public InSoloGame(IBeatmapInfo beatmapInfo, RulesetInfo ruleset) + public InSoloGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) : base(beatmapInfo, ruleset) { } From 2acf46154af93674dcb178e117dae20116243198 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 18:16:29 +0900 Subject: [PATCH 176/419] Remove many unused resolutions of `RulesetStore` --- .../TestSceneMatchBeatmapDetailArea.cs | 9 -------- .../TestSceneTournamentBeatmapPanel.cs | 10 +------- .../Leaderboards/UserTopScoreContainer.cs | 5 ---- .../BeatmapListingFilterControl.cs | 4 ---- osu.Game/Overlays/BeatmapSetOverlay.cs | 5 ---- .../Sections/PaginatedProfileSubsection.cs | 20 +++++++--------- .../Overlays/Rankings/SpotlightsLayout.cs | 23 ++++++++----------- osu.Game/Screens/Play/SoloSpectator.cs | 5 ---- osu.Game/Screens/Select/BeatmapDetails.cs | 4 ---- 9 files changed, 19 insertions(+), 66 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index d66603a448..1d61a5d496 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -2,11 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Beatmaps; using osu.Game.Online.Rooms; -using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay.Components; @@ -18,12 +15,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene { - [Resolved] - private BeatmapManager beatmapManager { get; set; } - - [Resolved] - private RulesetStore rulesetStore { get; set; } - [SetUp] public new void Setup() => Schedule(() => { diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs index 8139387a96..7132655535 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs @@ -3,28 +3,20 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets; using osu.Game.Tournament.Components; namespace osu.Game.Tournament.Tests.Components { public class TestSceneTournamentBeatmapPanel : TournamentTestScene { - [Resolved] - private IAPIProvider api { get; set; } - - [Resolved] - private RulesetStore rulesets { get; set; } - [BackgroundDependencyLoader] private void load() { var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = 1091460 }); req.Success += success; - api.Queue(req); + API.Queue(req); } private void success(APIBeatmap beatmap) diff --git a/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs index ab4210251e..3db497bd6a 100644 --- a/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs +++ b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs @@ -3,13 +3,11 @@ using System; using System.Threading; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets; using osuTK; namespace osu.Game.Online.Leaderboards @@ -25,9 +23,6 @@ namespace osu.Game.Online.Leaderboards protected override bool StartHidden => true; - [Resolved] - private RulesetStore rulesets { get; set; } - public UserTopScoreContainer(Func createScoreDelegate) { this.createScoreDelegate = createScoreDelegate; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 38f2bdb34f..f5b4785264 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -15,7 +15,6 @@ using osu.Framework.Threading; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets; using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; @@ -61,9 +60,6 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private IAPIProvider api { get; set; } - [Resolved] - private RulesetStore rulesets { get; set; } - public BeatmapListingFilterControl() { RelativeSizeAxes = Axes.X; diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index fa5a7c66d0..b9d3854066 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -12,7 +11,6 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.BeatmapSet; using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Overlays.Comments; -using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; @@ -24,9 +22,6 @@ namespace osu.Game.Overlays public const float Y_PADDING = 25; public const float RIGHT_WIDTH = 275; - [Resolved] - private RulesetStore rulesets { get; set; } - private readonly Bindable beatmapSet = new Bindable(); // receive input outside our bounds so we can trigger a close event on ourselves. diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs index affe9ecb0c..130ae44273 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs @@ -1,21 +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 osuTK; +using System.Collections.Generic; +using System.Linq; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Online.API; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics; using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osuTK; namespace osu.Game.Overlays.Profile.Sections { @@ -24,9 +23,6 @@ namespace osu.Game.Overlays.Profile.Sections [Resolved] private IAPIProvider api { get; set; } - [Resolved] - protected RulesetStore Rulesets { get; private set; } - protected int VisiblePages; protected int ItemsPerPage; diff --git a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs index cc553ad361..a37f762532 100644 --- a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs +++ b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs @@ -1,21 +1,21 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics; -using osu.Framework.Bindables; -using osu.Game.Rulesets; -using osu.Framework.Graphics.Containers; -using osu.Game.Online.API.Requests.Responses; -using osuTK; -using osu.Framework.Allocation; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; -using osu.Game.Overlays.Rankings.Tables; using System.Linq; using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays.Rankings.Tables; +using osu.Game.Rulesets; +using osuTK; namespace osu.Game.Overlays.Rankings { @@ -29,9 +29,6 @@ namespace osu.Game.Overlays.Rankings [Resolved] private IAPIProvider api { get; set; } - [Resolved] - private RulesetStore rulesets { get; set; } - private CancellationTokenSource cancellationToken; private GetSpotlightRankingsRequest getRankingsRequest; private GetSpotlightsRequest spotlightsRequest; diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index 45601999a0..7fea44b3ea 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -23,12 +23,10 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Spectator; using osu.Game.Overlays; using osu.Game.Overlays.Settings; -using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.Spectate; using osu.Game.Users; using osuTK; -using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; namespace osu.Game.Screens.Play { @@ -44,9 +42,6 @@ namespace osu.Game.Screens.Play [Resolved] private PreviewTrackManager previewTrackManager { get; set; } - [Resolved] - private RulesetStore rulesets { get; set; } - [Resolved] private BeatmapManager beatmaps { get; set; } diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 7543c89f17..bbe0a37d8e 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -16,7 +16,6 @@ using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Overlays.BeatmapSet; -using osu.Game.Rulesets; using osu.Game.Screens.Select.Details; using osuTK; using osuTK.Graphics; @@ -38,9 +37,6 @@ namespace osu.Game.Screens.Select [Resolved] private IAPIProvider api { get; set; } - [Resolved] - private RulesetStore rulesets { get; set; } - private IBeatmapInfo beatmapInfo; private APIFailTimes failTimes; From 1eed2436e6d76b94312541b01e1fd7b0baf8632e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 18:37:45 +0900 Subject: [PATCH 177/419] Clean up unused resolved properties --- osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs | 7 ------- osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs | 4 ---- osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs | 3 --- osu.Game.Rulesets.Taiko/UI/InputDrum.cs | 4 ---- .../Beatmaps/TestSceneBeatmapDifficultyCache.cs | 3 --- osu.Game.Tests/Input/ConfineMouseTrackerTest.cs | 3 --- osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs | 5 ----- .../Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs | 4 ---- .../Multiplayer/TestSceneMatchBeatmapDetailArea.cs | 9 --------- .../Visual/Multiplayer/TestScenePlaylistsSongSelect.cs | 3 --- osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs | 3 --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 4 ---- .../Visual/Online/TestSceneUserProfileOverlay.cs | 5 ----- .../Ranking/TestSceneContractedPanelMiddleContent.cs | 4 ---- .../TestSceneUpdateableBeatmapBackgroundSprite.cs | 3 --- .../Components/TestSceneTournamentBeatmapPanel.cs | 4 ---- .../Screens/Editors/SeedingEditorScreen.cs | 6 ------ .../Screens/Setup/StablePathSelectScreen.cs | 3 --- osu.Game/Audio/PreviewTrackManager.cs | 3 --- osu.Game/Collections/CollectionFilterDropdown.cs | 4 ---- osu.Game/Collections/CollectionManager.cs | 3 --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 3 --- osu.Game/Online/Leaderboards/UserTopScoreContainer.cs | 5 ----- .../BeatmapListing/BeatmapListingFilterControl.cs | 4 ---- osu.Game/Overlays/BeatmapSetOverlay.cs | 5 ----- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 3 --- osu.Game/Overlays/Rankings/SpotlightsLayout.cs | 3 --- osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs | 5 ----- osu.Game/Rulesets/UI/Playfield.cs | 4 ---- .../Timelines/Summary/Parts/GroupVisualisation.cs | 5 ----- .../Screens/Edit/Compose/Components/Timeline/Timeline.cs | 3 --- .../Components/Timeline/TimelineControlPointGroup.cs | 5 ----- .../Components/Timeline/TimelineHitObjectBlueprint.cs | 3 --- osu.Game/Screens/Edit/Editor.cs | 3 --- osu.Game/Screens/Edit/EditorRoundedScreen.cs | 4 ---- osu.Game/Screens/Edit/EditorTable.cs | 3 --- .../Screens/Edit/Setup/FileChooserLabelledTextBox.cs | 4 ---- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 3 --- osu.Game/Screens/Edit/Verify/IssueList.cs | 3 --- osu.Game/Screens/Menu/StorageErrorDialog.cs | 3 --- osu.Game/Screens/OnlinePlay/Components/RoomManager.cs | 4 ---- .../OnlinePlay/Components/SelectionPollingComponent.cs | 4 ---- .../Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs | 3 --- .../OnlinePlay/Lounge/Components/RoomsContainer.cs | 3 --- .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 8 -------- .../Multiplayer/Match/MultiplayerReadyButton.cs | 4 ---- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 3 --- osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs | 9 --------- .../OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 4 ---- .../Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs | 5 ----- osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs | 3 --- osu.Game/Screens/Play/HUD/DefaultComboCounter.cs | 3 --- osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs | 3 --- osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 3 --- osu.Game/Screens/Play/SoloSpectator.cs | 4 ---- osu.Game/Screens/Ranking/ScorePanelList.cs | 4 ---- osu.Game/Screens/Select/BeatmapDetails.cs | 4 ---- .../Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 3 --- osu.Game/Skinning/Editor/SkinEditorOverlay.cs | 5 ----- osu.Game/Skinning/LegacyAccuracyCounter.cs | 5 ----- osu.Game/Skinning/PoolableSkinnableSample.cs | 4 ---- osu.Game/Skinning/SkinnableSound.cs | 4 ---- osu.Game/Updater/NoActionUpdateManager.cs | 4 ---- 63 files changed, 254 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 9d1f5429a1..1aa20f4737 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -9,7 +9,6 @@ using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; -using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.UI; @@ -46,12 +45,6 @@ namespace osu.Game.Rulesets.Mania.Edit [Resolved] private EditorBeatmap beatmap { get; set; } - [Resolved] - private IScrollingInfo scrollingInfo { get; set; } - - [Resolved] - private Bindable working { get; set; } - [Resolved] private OsuColour colours { get; set; } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index dc858fb54f..9fe1eb7932 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -7,16 +7,12 @@ using osu.Framework.Allocation; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit.Compose.Components; namespace osu.Game.Rulesets.Mania.Edit { public class ManiaSelectionHandler : EditorSelectionHandler { - [Resolved] - private IScrollingInfo scrollingInfo { get; set; } - [Resolved] private HitObjectComposer composer { get; set; } diff --git a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs index 90d3c6c4c7..9f4963b022 100644 --- a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs @@ -15,9 +15,6 @@ namespace osu.Game.Rulesets.Mania.UI public JudgementResult Result { get; private set; } - [Resolved] - private Column column { get; set; } - private SkinnableDrawable skinnableExplosion; public PoolableHitExplosion() diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index 861b800038..16be20f7f3 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -12,7 +12,6 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; @@ -149,9 +148,6 @@ namespace osu.Game.Rulesets.Taiko.UI centreHit.Colour = colours.Pink; } - [Resolved(canBeNull: true)] - private GameplayClock gameplayClock { get; set; } - public bool OnPressed(KeyBindingPressEvent e) { Drawable target = null; diff --git a/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs b/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs index 2a60a7b96d..3a82cbc785 100644 --- a/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs +++ b/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs @@ -25,9 +25,6 @@ namespace osu.Game.Tests.Beatmaps private BeatmapSetInfo importedSet; - [Resolved] - private BeatmapManager beatmaps { get; set; } - private TestBeatmapDifficultyCache difficultyCache; private IBindable starDifficultyBindable; diff --git a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs index b612899d79..28937b2120 100644 --- a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs +++ b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs @@ -18,9 +18,6 @@ namespace osu.Game.Tests.Input [Resolved] private FrameworkConfigManager frameworkConfigManager { get; set; } - [Resolved] - private OsuConfigManager osuConfigManager { get; set; } - [TestCase(WindowMode.Windowed)] [TestCase(WindowMode.Borderless)] public void TestDisableConfining(WindowMode windowMode) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index a0b27755b7..a0602e21b9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -2,12 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; -using osu.Game.Skinning; using osu.Game.Skinning.Editor; namespace osu.Game.Tests.Visual.Gameplay @@ -16,9 +14,6 @@ namespace osu.Game.Tests.Visual.Gameplay { private SkinEditor skinEditor; - [Resolved] - private SkinManager skinManager { get; set; } - protected override bool Autoplay => true; [SetUpSteps] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index 723e35ed55..3074a91dc6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -10,7 +10,6 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; -using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -36,9 +35,6 @@ namespace osu.Game.Tests.Visual.Gameplay private Drawable hideTarget => hudOverlay.KeyCounter; private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First(); - [Resolved] - private OsuConfigManager config { get; set; } - [Test] public void TestComboCounterIncrementing() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index d66603a448..1d61a5d496 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -2,11 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Beatmaps; using osu.Game.Online.Rooms; -using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay.Components; @@ -18,12 +15,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene { - [Resolved] - private BeatmapManager beatmapManager { get; set; } - - [Resolved] - private RulesetStore rulesetStore { get; set; } - [SetUp] public new void Setup() => Schedule(() => { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 35c66e8cda..5aac228f4b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -24,9 +24,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestScenePlaylistsSongSelect : OnlinePlayTestScene { - [Resolved] - private BeatmapManager beatmapManager { get; set; } - private BeatmapManager manager; private RulesetStore rulesets; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs index 2706ff5ceb..4d1e279090 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs @@ -83,9 +83,6 @@ namespace osu.Game.Tests.Visual.Navigation [Resolved] private OsuGameBase gameBase { get; set; } - [Resolved] - private GameHost host { get; set; } - [Test] public void TestNullRulesetHandled() { diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 7028ecf39f..9c65b2dc51 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; -using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -47,9 +46,6 @@ namespace osu.Game.Tests.Visual.Online [CanBeNull] private Func> onGetMessages; - [Resolved] - private GameHost host { get; set; } - public TestSceneChatOverlay() { channels = Enumerable.Range(1, 10) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index ce8136199f..1c92bb1e38 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -4,8 +4,6 @@ using System; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Profile; @@ -20,9 +18,6 @@ namespace osu.Game.Tests.Visual.Online private readonly TestUserProfileOverlay profile; - [Resolved] - private IAPIProvider api { get; set; } - public static readonly APIUser TEST_USER = new APIUser { Username = @"Somebody", diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs index acacdf8644..f246560c82 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; -using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens.Ranking; @@ -20,9 +19,6 @@ namespace osu.Game.Tests.Visual.Ranking { public class TestSceneContractedPanelMiddleContent : OsuTestScene { - [Resolved] - private RulesetStore rulesetStore { get; set; } - [Test] public void TestShowPanel() { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs index d30f1e8889..3fa9b8b877 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs @@ -26,9 +26,6 @@ namespace osu.Game.Tests.Visual.UserInterface private BeatmapSetInfo testBeatmap; private IAPIProvider api; - [Resolved] - private BeatmapManager beatmaps { get; set; } - [BackgroundDependencyLoader] private void load(OsuGameBase osu, IAPIProvider api, RulesetStore rulesets) { diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs index 8139387a96..a96c5123e0 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets; using osu.Game.Tournament.Components; namespace osu.Game.Tournament.Tests.Components @@ -16,9 +15,6 @@ namespace osu.Game.Tournament.Tests.Components [Resolved] private IAPIProvider api { get; set; } - [Resolved] - private RulesetStore rulesets { get; set; } - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index 9abf1d3adb..5d2fddffd9 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -25,9 +25,6 @@ namespace osu.Game.Tournament.Screens.Editors protected override BindableList Storage => team.SeedingResults; - [Resolved(canBeNull: true)] - private TournamentSceneManager sceneManager { get; set; } - public SeedingEditorScreen(TournamentTeam team, TournamentScreen parentScreen) : base(parentScreen) { @@ -38,9 +35,6 @@ namespace osu.Game.Tournament.Screens.Editors { public SeedingResult Model { get; } - [Resolved] - private LadderInfo ladderInfo { get; set; } - public SeedingResultRow(TournamentTeam team, SeedingResult round) { Model = round; diff --git a/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs index 8e9b32231f..5a1ceecd01 100644 --- a/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs @@ -21,9 +21,6 @@ namespace osu.Game.Tournament.Screens.Setup { public class StablePathSelectScreen : TournamentScreen { - [Resolved] - private GameHost host { get; set; } - [Resolved(canBeNull: true)] private TournamentSceneManager sceneManager { get; set; } diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs index e631d35180..6d56d152f1 100644 --- a/osu.Game/Audio/PreviewTrackManager.cs +++ b/osu.Game/Audio/PreviewTrackManager.cs @@ -18,9 +18,6 @@ namespace osu.Game.Audio private readonly BindableDouble muteBindable = new BindableDouble(); - [Resolved] - private AudioManager audio { get; set; } - private ITrackStore trackStore; protected TrackManagerPreviewTrack CurrentTrack; diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index ad23874b2e..77bda00107 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osuTK; @@ -193,9 +192,6 @@ namespace osu.Game.Collections [NotNull] protected new CollectionFilterMenuItem Item => ((DropdownMenuItem)base.Item).Value; - [Resolved] - private OsuColour colours { get; set; } - [Resolved] private IBindable beatmap { get; set; } diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index 9ff92032b7..c4f991094c 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -40,9 +40,6 @@ namespace osu.Game.Collections public readonly BindableList Collections = new BindableList(); - [Resolved] - private GameHost host { get; set; } - [Resolved] private BeatmapManager beatmaps { get; set; } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index e01c7c9e49..644c2e2a99 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -65,9 +65,6 @@ namespace osu.Game.Online.Leaderboards [Resolved(CanBeNull = true)] private SongSelect songSelect { get; set; } - [Resolved] - private ScoreManager scoreManager { get; set; } - [Resolved] private Storage storage { get; set; } diff --git a/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs index ab4210251e..3db497bd6a 100644 --- a/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs +++ b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs @@ -3,13 +3,11 @@ using System; using System.Threading; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets; using osuTK; namespace osu.Game.Online.Leaderboards @@ -25,9 +23,6 @@ namespace osu.Game.Online.Leaderboards protected override bool StartHidden => true; - [Resolved] - private RulesetStore rulesets { get; set; } - public UserTopScoreContainer(Func createScoreDelegate) { this.createScoreDelegate = createScoreDelegate; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 38f2bdb34f..f5b4785264 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -15,7 +15,6 @@ using osu.Framework.Threading; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets; using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; @@ -61,9 +60,6 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private IAPIProvider api { get; set; } - [Resolved] - private RulesetStore rulesets { get; set; } - public BeatmapListingFilterControl() { RelativeSizeAxes = Axes.X; diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index fa5a7c66d0..b9d3854066 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -12,7 +11,6 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.BeatmapSet; using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Overlays.Comments; -using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; @@ -24,9 +22,6 @@ namespace osu.Game.Overlays public const float Y_PADDING = 25; public const float RIGHT_WIDTH = 275; - [Resolved] - private RulesetStore rulesets { get; set; } - private readonly Bindable beatmapSet = new Bindable(); // receive input outside our bounds so we can trigger a close event on ourselves. diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 269ed81bb5..0844975906 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -43,9 +43,6 @@ namespace osu.Game.Overlays.Dashboard }; } - [Resolved] - private IAPIProvider api { get; set; } - [Resolved] private UserLookupCache users { get; set; } diff --git a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs index cc553ad361..dbfcfea414 100644 --- a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs +++ b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs @@ -29,9 +29,6 @@ namespace osu.Game.Overlays.Rankings [Resolved] private IAPIProvider api { get; set; } - [Resolved] - private RulesetStore rulesets { get; set; } - private CancellationTokenSource cancellationToken; private GetSpotlightRankingsRequest getRankingsRequest; private GetSpotlightsRequest spotlightsRequest; diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs index 6f0b433acb..789ed457a4 100644 --- a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs +++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs @@ -5,19 +5,14 @@ using System.Linq; using Markdig.Extensions.Yaml; using Markdig.Syntax; using Markdig.Syntax.Inlines; -using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers.Markdown; using osu.Game.Graphics.Containers.Markdown; -using osu.Game.Online.API; namespace osu.Game.Overlays.Wiki.Markdown { public class WikiMarkdownContainer : OsuMarkdownContainer { - [Resolved] - private IAPIProvider api { get; set; } - public string CurrentPath { set => DocumentUrl = value; diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 52aecb27de..d0bbf859af 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -19,7 +19,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Skinning; using osuTK; using System.Diagnostics; -using osu.Framework.Audio.Sample; namespace osu.Game.Rulesets.UI { @@ -88,9 +87,6 @@ namespace osu.Game.Rulesets.UI [Resolved(CanBeNull = true)] private IReadOnlyList mods { get; set; } - [Resolved] - private ISampleStore sampleStore { get; set; } - /// /// Creates a new . /// diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs index 4629f9b540..f0e643f805 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs @@ -1,20 +1,15 @@ // 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.Beatmaps.ControlPoints; -using osu.Game.Graphics; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { public class GroupVisualisation : CompositeDrawable { - [Resolved] - private OsuColour colours { get; set; } - public readonly ControlPointGroup Group; private readonly IBindableList controlPoints = new BindableList(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index b8fa05e7eb..265f56534f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -279,9 +279,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline editorClock.Start(); } - [Resolved] - private EditorBeatmap beatmap { get; set; } - [Resolved] private IBeatSnapProvider beatSnapProvider { get; set; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs index 2b2e66fb18..9610f6424c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs @@ -1,12 +1,10 @@ // 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.Beatmaps.ControlPoints; -using osu.Game.Graphics; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { @@ -16,9 +14,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly IBindableList controlPoints = new BindableList(); - [Resolved] - private OsuColour colours { get; set; } - public TimelineControlPointGroup(ControlPointGroup group) { Group = group; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 80aa6972b1..1839b0507d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -184,9 +184,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private SamplePointPiece sampleOverrideDisplay; private DifficultyPointPiece difficultyOverrideDisplay; - [Resolved] - private EditorBeatmap beatmap { get; set; } - private DifficultyControlPoint difficultyControlPoint; private SampleControlPoint sampleControlPoint; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index ac71298f36..48489c60ab 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -109,9 +109,6 @@ namespace osu.Game.Screens.Edit [Resolved] private IAPIProvider api { get; set; } - [Resolved] - private MusicController music { get; set; } - [Cached] public readonly EditorClipboard Clipboard = new EditorClipboard(); diff --git a/osu.Game/Screens/Edit/EditorRoundedScreen.cs b/osu.Game/Screens/Edit/EditorRoundedScreen.cs index 7f7b3abc2a..62f40f0325 100644 --- a/osu.Game/Screens/Edit/EditorRoundedScreen.cs +++ b/osu.Game/Screens/Edit/EditorRoundedScreen.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; using osu.Game.Overlays; namespace osu.Game.Screens.Edit @@ -14,9 +13,6 @@ namespace osu.Game.Screens.Edit { public const int HORIZONTAL_PADDING = 100; - [Resolved] - private OsuColour colours { get; set; } - private Container roundedContent; protected override Container Content => roundedContent; diff --git a/osu.Game/Screens/Edit/EditorTable.cs b/osu.Game/Screens/Edit/EditorTable.cs index ab8bd6a3bc..a67a060134 100644 --- a/osu.Game/Screens/Edit/EditorTable.cs +++ b/osu.Game/Screens/Edit/EditorTable.cs @@ -62,9 +62,6 @@ namespace osu.Game.Screens.Edit private readonly Box hoveredBackground; - [Resolved] - private EditorClock clock { get; set; } - public RowBackground(object item) { Item = item; diff --git a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs index f833bc49f7..d1e35ae20d 100644 --- a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs +++ b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Database; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osuTK; @@ -36,9 +35,6 @@ namespace osu.Game.Screens.Edit.Setup [Resolved] private OsuGameBase game { get; set; } - [Resolved] - private SectionsContainer sectionsContainer { get; set; } - public FileChooserLabelledTextBox(params string[] handledExtensions) { this.handledExtensions = handledExtensions; diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 7a98cf63c3..1e6899e05f 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -132,9 +132,6 @@ namespace osu.Game.Screens.Edit.Timing controlPoints.BindTo(group.ControlPoints); } - [Resolved] - private OsuColour colours { get; set; } - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index fd238feeac..cadcdebc6e 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -23,9 +23,6 @@ namespace osu.Game.Screens.Edit.Verify { private IssueTable table; - [Resolved] - private EditorClock clock { get; set; } - [Resolved] private IBindable workingBeatmap { get; set; } diff --git a/osu.Game/Screens/Menu/StorageErrorDialog.cs b/osu.Game/Screens/Menu/StorageErrorDialog.cs index dcaad4013a..250623ec68 100644 --- a/osu.Game/Screens/Menu/StorageErrorDialog.cs +++ b/osu.Game/Screens/Menu/StorageErrorDialog.cs @@ -15,9 +15,6 @@ namespace osu.Game.Screens.Menu [Resolved] private DialogOverlay dialogOverlay { get; set; } - [Resolved] - private OsuGameBase osuGame { get; set; } - public StorageErrorDialog(OsuStorage storage, OsuStorageError error) { HeaderText = "osu! storage error"; diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index ddfdab18f7..47055c9c36 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; -using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; @@ -30,9 +29,6 @@ namespace osu.Game.Screens.OnlinePlay.Components [Resolved] private RulesetStore rulesets { get; set; } - [Resolved] - private BeatmapManager beatmaps { get; set; } - [Resolved] private IAPIProvider api { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs index b9d2bdf23e..22842fbb9e 100644 --- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading.Tasks; -using osu.Framework.Allocation; using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components @@ -12,9 +11,6 @@ namespace osu.Game.Screens.OnlinePlay.Components /// public class SelectionPollingComponent : RoomPollingComponent { - [Resolved] - private IRoomManager roomManager { get; set; } - private readonly Room room; public SelectionPollingComponent(Room room) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 9920883078..0502c4abe6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -30,9 +30,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public readonly Room Room; - [Resolved] - private BeatmapManager beatmaps { get; set; } - protected Container ButtonsContainer { get; private set; } private readonly Bindable roomType = new Bindable(); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 54c762b8ce..f4d7823fcc 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -33,9 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components [Resolved] private IRoomManager roomManager { get; set; } - [Resolved(CanBeNull = true)] - private LoungeSubScreen loungeSubScreen { get; set; } - // handle deselection public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 34edc1ccd1..39d60a0b05 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; -using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -20,7 +19,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; -using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Match.Components; using osuTK; @@ -84,12 +82,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [Resolved] private MultiplayerClient client { get; set; } - [Resolved] - private Bindable beatmap { get; set; } - - [Resolved] - private Bindable ruleset { get; set; } - [Resolved] private OngoingOperationTracker ongoingOperationTracker { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index ce988e377f..874113d859 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics; using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; -using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Screens.OnlinePlay.Components; using osuTK; @@ -25,9 +24,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match set => button.Action = value; } - [Resolved] - private IAPIProvider api { get; set; } - [Resolved] private OsuColour colours { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 57d0d2c198..9ac64add9a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -36,9 +36,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate [Resolved] private OsuColour colours { get; set; } - [Resolved] - private SpectatorClient spectatorClient { get; set; } - [Resolved] private MultiplayerClient multiplayerClient { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index a18e4b45cf..19153521cd 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -40,18 +40,9 @@ namespace osu.Game.Screens.OnlinePlay [Cached] private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); - [Resolved(CanBeNull = true)] - private MusicController music { get; set; } - - [Resolved] - private OsuGameBase game { get; set; } - [Resolved] protected IAPIProvider API { get; private set; } - [Resolved(CanBeNull = true)] - private OsuLogo logo { get; set; } - protected OnlinePlayScreen() { Anchor = Anchor.Centre; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 6d2a426e70..7e045802f7 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Input; -using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; @@ -29,9 +28,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public override string ShortTitle => "playlist"; - [Resolved] - private IAPIProvider api { get; set; } - private readonly IBindable isIdle = new BindableBool(); private MatchLeaderboard leaderboard; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index 03c95ec060..0fd76f7e25 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -2,9 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Screens; -using osu.Game.Beatmaps; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.Select; @@ -13,9 +11,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { public class PlaylistsSongSelect : OnlinePlaySongSelect { - [Resolved] - private BeatmapManager beatmaps { get; set; } - public PlaylistsSongSelect(Room room) : base(room) { diff --git a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs index 324e5d43b5..06b53e8426 100644 --- a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs @@ -9,9 +9,6 @@ namespace osu.Game.Screens.Play.HUD { public class DefaultAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable { - [Resolved(canBeNull: true)] - private HUDOverlay hud { get; set; } - public bool UsesFixedAnchor { get; set; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs index 6d87211ddc..52f86d2bc3 100644 --- a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs @@ -15,9 +15,6 @@ namespace osu.Game.Screens.Play.HUD { public class DefaultComboCounter : RollingCounter, ISkinnableDrawable { - [Resolved(canBeNull: true)] - private HUDOverlay hud { get; set; } - public bool UsesFixedAnchor { get; set; } public DefaultComboCounter() diff --git a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs index 87b19e8433..6af89404e0 100644 --- a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs @@ -16,9 +16,6 @@ namespace osu.Game.Screens.Play.HUD Origin = Anchor.TopCentre; } - [Resolved(canBeNull: true)] - private HUDOverlay hud { get; set; } - public bool UsesFixedAnchor { get; set; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 5c5b66d496..4859f1b977 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -40,9 +40,6 @@ namespace osu.Game.Screens.Play.HUD private bool isRolling; - [Resolved] - private ISkinSource skin { get; set; } - private readonly Container counterContainer; /// diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index 45601999a0..b48c787752 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -23,7 +23,6 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Spectator; using osu.Game.Overlays; using osu.Game.Overlays.Settings; -using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.Spectate; using osu.Game.Users; @@ -44,9 +43,6 @@ namespace osu.Game.Screens.Play [Resolved] private PreviewTrackManager previewTrackManager { get; set; } - [Resolved] - private RulesetStore rulesets { get; set; } - [Resolved] private BeatmapManager beatmaps { get; set; } diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index d5b8a4c8ea..22be91b974 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -13,7 +13,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; -using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; using osu.Game.Scoring; using osuTK; @@ -70,9 +69,6 @@ namespace osu.Game.Screens.Ranking [Resolved] private ScoreManager scoreManager { get; set; } - [Resolved] - private BeatmapDifficultyCache difficultyCache { get; set; } - private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource(); private readonly Flow flow; private readonly Scroll scroll; diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 7543c89f17..bbe0a37d8e 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -16,7 +16,6 @@ using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Overlays.BeatmapSet; -using osu.Game.Rulesets; using osu.Game.Screens.Select.Details; using osuTK; using osuTK.Graphics; @@ -38,9 +37,6 @@ namespace osu.Game.Screens.Select [Resolved] private IAPIProvider api { get; set; } - [Resolved] - private RulesetStore rulesets { get; set; } - private IBeatmapInfo beatmapInfo; private APIFailTimes failTimes; diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 1fd6d8c921..872e630ba0 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -64,9 +64,6 @@ namespace osu.Game.Screens.Select.Leaderboards [Resolved] private ScoreManager scoreManager { get; set; } - [Resolved] - private BeatmapDifficultyCache difficultyCache { get; set; } - [Resolved] private IBindable ruleset { get; set; } diff --git a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs index d27122aea8..340c6ed931 100644 --- a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs +++ b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs @@ -3,13 +3,11 @@ using System.Diagnostics; using JetBrains.Annotations; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; @@ -28,9 +26,6 @@ namespace osu.Game.Skinning.Editor public const float VISIBLE_TARGET_SCALE = 0.8f; - [Resolved] - private OsuColour colours { get; set; } - public SkinEditorOverlay(ScalingContainer target) { this.target = target; diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index fd5a9500d9..bdcb85456a 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -1,10 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osuTK; @@ -23,9 +21,6 @@ namespace osu.Game.Skinning Margin = new MarginPadding(10); } - [Resolved(canBeNull: true)] - private HUDOverlay hud { get; set; } - protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(LegacyFont.Score) { Anchor = Anchor.TopRight, diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index 3fcca74fb8..5db4f00b46 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -4,7 +4,6 @@ using System; using System.Linq; using JetBrains.Annotations; -using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; @@ -30,9 +29,6 @@ namespace osu.Game.Skinning private ISampleInfo sampleInfo; private SampleChannel activeChannel; - [Resolved] - private ISampleStore sampleStore { get; set; } - /// /// Creates a new with no applied . /// An can be applied later via . diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index f935adf7a5..c9e55c09aa 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -7,7 +7,6 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; @@ -43,9 +42,6 @@ namespace osu.Game.Skinning private readonly AudioContainer samplesContainer; - [Resolved] - private ISampleStore sampleStore { get; set; } - [Resolved(CanBeNull = true)] private IPooledSampleProvider samplePool { get; set; } diff --git a/osu.Game/Updater/NoActionUpdateManager.cs b/osu.Game/Updater/NoActionUpdateManager.cs index 641263ed0f..8f9c4c6f16 100644 --- a/osu.Game/Updater/NoActionUpdateManager.cs +++ b/osu.Game/Updater/NoActionUpdateManager.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; -using osu.Framework.Platform; using osu.Game.Online.API; using osu.Game.Overlays.Notifications; @@ -19,9 +18,6 @@ namespace osu.Game.Updater { private string version; - [Resolved] - private GameHost host { get; set; } - [BackgroundDependencyLoader] private void load(OsuGameBase game) { From d5803e541bdfcc1b0f6ed703bdb0773b766b5624 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 3 Dec 2021 20:05:25 +0900 Subject: [PATCH 178/419] Give playlist items a PlayedAt date --- .../TestSceneMultiplayerPlaylist.cs | 4 ++- .../Online/Multiplayer/MultiplayerClient.cs | 3 +- .../Online/Rooms/MultiplayerPlaylistItem.cs | 8 ++--- osu.Game/Online/Rooms/PlaylistItem.cs | 3 ++ .../Match/Playlist/MultiplayerHistoryList.cs | 2 +- .../Multiplayer/TestMultiplayerClient.cs | 36 +++++++------------ 6 files changed, 26 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index ae885685f7..674ee0f186 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -168,7 +169,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { Beatmap = { Value = importedBeatmap }, BeatmapID = importedBeatmap.OnlineID ?? -1, - Expired = expired + Expired = expired, + PlayedAt = DateTimeOffset.Now }))); /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index ae53ef2e52..555558eb6d 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -742,7 +742,8 @@ namespace osu.Game.Online.Multiplayer Beatmap = { Value = apiBeatmap }, Ruleset = { Value = ruleset }, Expired = item.Expired, - PlaylistOrder = item.PlaylistOrder + PlaylistOrder = item.PlaylistOrder, + PlayedAt = item.PlayedAt }; playlistItem.RequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))); diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index c84b5b9039..1c33b79531 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -46,11 +46,10 @@ namespace osu.Game.Online.Rooms public ushort PlaylistOrder { get; set; } /// - /// The date when this was last updated. - /// Not serialised to/from the client. + /// The date when this was played. /// - [IgnoreMember] - public DateTimeOffset UpdatedAt { get; set; } + [Key(9)] + public DateTimeOffset? PlayedAt { get; set; } public MultiplayerPlaylistItem() { @@ -66,6 +65,7 @@ namespace osu.Game.Online.Rooms AllowedMods = item.AllowedMods.Select(m => new APIMod(m)).ToArray(); Expired = item.Expired; PlaylistOrder = item.PlaylistOrder ?? 0; + PlayedAt = item.PlayedAt; } } } diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index beefb17d54..4d67864048 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -36,6 +36,9 @@ namespace osu.Game.Online.Rooms [JsonProperty("playlist_order")] public ushort? PlaylistOrder { get; set; } + [JsonProperty("played_at")] + public DateTimeOffset? PlayedAt { get; set; } + [JsonIgnore] public IBindable Valid => valid; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs index ed41a6fc78..d708b39898 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist private class HistoryFillFlowContainer : FillFlowContainer> { - public override IEnumerable FlowingChildren => base.FlowingChildren.OfType>().OrderByDescending(item => item.Model.PlaylistOrder); + public override IEnumerable FlowingChildren => base.FlowingChildren.OfType>().OrderByDescending(item => item.Model.PlayedAt); } } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index f2d5323386..024b091bfb 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -400,7 +400,7 @@ namespace osu.Game.Tests.Visual.Multiplayer // Expire the current playlist item. currentItem.Expired = true; - currentItem.UpdatedAt = DateTimeOffset.Now; + currentItem.PlayedAt = DateTimeOffset.Now; await ((IMultiplayerClient)this).PlaylistItemChanged(currentItem).ConfigureAwait(false); await updatePlaylistOrder(Room).ConfigureAwait(false); @@ -430,9 +430,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { Debug.Assert(Room != null); - // Some tests can add items in already-expired states. - item.UpdatedAt = DateTimeOffset.Now; - // Add the item to the list first in order to compute gameplay order. serverSidePlaylist.Add(item); await updatePlaylistOrder(Room).ConfigureAwait(false); @@ -443,8 +440,12 @@ namespace osu.Game.Tests.Visual.Multiplayer private async Task updateCurrentItem(MultiplayerRoom room, bool notify = true) { - // The playlist is already in correct gameplay order, so pick the next non-expired item or default to the last item. - MultiplayerPlaylistItem nextItem = serverSidePlaylist.FirstOrDefault(i => !i.Expired) ?? room.Playlist.Last(); + MultiplayerPlaylistItem nextItem = serverSidePlaylist + .Where(i => !i.Expired) + .OrderBy(i => i.PlaylistOrder) + .FirstOrDefault() + ?? room.Playlist.Last(); + currentIndex = serverSidePlaylist.IndexOf(nextItem); long lastItem = room.Settings.PlaylistItemId; @@ -494,29 +495,18 @@ namespace osu.Game.Tests.Visual.Multiplayer break; } - // For expired items, it's important that they're ordered in ascending order such that the last updated item is the last in the list. - // This is so that the updated_at database column doesn't get refreshed as a result of change in ordering. - List orderedExpiredItems = serverSidePlaylist.Where(item => item.Expired).OrderBy(item => item.UpdatedAt).ToList(); - for (int i = 0; i < orderedExpiredItems.Count; i++) - await setOrder(orderedExpiredItems[i], (ushort)i).ConfigureAwait(false); - for (int i = 0; i < orderedActiveItems.Count; i++) - await setOrder(orderedActiveItems[i], (ushort)i).ConfigureAwait(false); - - serverSidePlaylist.Clear(); - serverSidePlaylist.AddRange(orderedExpiredItems); - serverSidePlaylist.AddRange(orderedActiveItems); - - async Task setOrder(MultiplayerPlaylistItem item, ushort order) { - if (item.PlaylistOrder == order) - return; + var item = orderedActiveItems[i]; - item.PlaylistOrder = order; + if (item.PlaylistOrder == i) + continue; + + item.PlaylistOrder = (ushort)i; // Items which have an ID of 0 are not in the database, so avoid propagating database/hub events for them. if (item.ID <= 0) - return; + continue; await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); } From 9acc0556a4fbe260a5cea97f9b823cdd2aa893df Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 3 Dec 2021 20:35:47 +0900 Subject: [PATCH 179/419] Remove unused event --- osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs index 2c9eb76dfc..e42a10430a 100644 --- a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs +++ b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs @@ -26,15 +26,10 @@ namespace osu.Desktop.LegacyIpc { private static readonly Logger logger = Logger.GetLogger("legacy-ipc"); - /// - /// Invoked when a message is received from a legacy client. - /// - public new Func? MessageReceived; - public LegacyTcpIpcProvider() : base(45357) { - base.MessageReceived += msg => + MessageReceived += msg => { try { From 34b0e374d855cacde830c91b7251f74d499d1a1d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 3 Dec 2021 21:29:20 +0900 Subject: [PATCH 180/419] Add serialisation/deserialisation explanation --- osu.Desktop/LegacyIpc/LegacyIpcMessage.cs | 13 +++++++++++++ osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs | 1 + 2 files changed, 14 insertions(+) diff --git a/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs b/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs index 6fefae4509..0fa60e2068 100644 --- a/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs +++ b/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs @@ -2,11 +2,24 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Platform; +using Newtonsoft.Json.Linq; namespace osu.Desktop.LegacyIpc { /// /// An that can be used to communicate to and from legacy clients. + /// + /// In order to deserialise types at either end, types must be serialised as their , + /// however this cannot be done since osu!stable and osu!lazer live in two different assemblies. + ///
+ /// To get around this, this class exists which serialises a payload () as an type, + /// which can be deserialised at either end because it is part of the core library (mscorlib / System.Private.CorLib). + /// The payload contains the data to be sent over the IPC channel. + ///
+ /// At either end, Json.NET deserialises the payload into a which is manually converted back into the expected type, + /// which then further contains another representing the data sent over the IPC channel whose type can likewise be lazily matched through + /// . + ///
///
/// /// Synchronise any changes with osu-stable. diff --git a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs index e42a10430a..97a4c57bf0 100644 --- a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs +++ b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs @@ -36,6 +36,7 @@ namespace osu.Desktop.LegacyIpc logger.Add("Processing legacy IPC message..."); logger.Add($" {msg.Value}", LogLevel.Debug); + // See explanation in LegacyIpcMessage for why this is done this way. var legacyData = ((JObject)msg.Value).ToObject(); object value = parseObject((JObject)legacyData!.MessageData, legacyData.MessageType); From b3b239c9a138067cfdc6d0907f7194bd959aa959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 4 Dec 2021 13:57:39 +0100 Subject: [PATCH 181/419] Fix test failures due to beatmap lookup logic being active even when model is populated --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index dae7b611f2..de82c463b3 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -157,8 +157,11 @@ namespace osu.Game.Screens.OnlinePlay Schedule(() => ownerAvatar.User = foundUser); } - var foundBeatmap = await beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ConfigureAwait(false); - Schedule(() => Item.Beatmap.Value = foundBeatmap); + if (Item.Beatmap.Value == null) + { + var foundBeatmap = await beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ConfigureAwait(false); + Schedule(() => Item.Beatmap.Value = foundBeatmap); + } } catch (Exception e) { From 85d3b70d8ce096011172daf8a4dede7100aa75a7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 4 Dec 2021 22:34:37 +0900 Subject: [PATCH 182/419] Update test multiplayer client to match server-side --- .../Multiplayer/TestMultiplayerClient.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 024b091bfb..f5f5eebe0f 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -431,6 +431,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Debug.Assert(Room != null); // Add the item to the list first in order to compute gameplay order. + item.ID = long.MaxValue; serverSidePlaylist.Add(item); await updatePlaylistOrder(Room).ConfigureAwait(false); @@ -440,11 +441,9 @@ namespace osu.Game.Tests.Visual.Multiplayer private async Task updateCurrentItem(MultiplayerRoom room, bool notify = true) { - MultiplayerPlaylistItem nextItem = serverSidePlaylist - .Where(i => !i.Expired) - .OrderBy(i => i.PlaylistOrder) - .FirstOrDefault() - ?? room.Playlist.Last(); + // Pick the next non-expired playlist item by playlist order, or default to the most-recently-expired item. + MultiplayerPlaylistItem nextItem = serverSidePlaylist.Where(i => !i.Expired).OrderBy(i => i.PlaylistOrder).FirstOrDefault() + ?? serverSidePlaylist.OrderByDescending(i => i.PlayedAt).First(); currentIndex = serverSidePlaylist.IndexOf(nextItem); @@ -462,7 +461,7 @@ namespace osu.Game.Tests.Visual.Multiplayer switch (room.Settings.QueueMode) { default: - orderedActiveItems = serverSidePlaylist.Where(item => !item.Expired).OrderBy(item => item.ID == 0 ? int.MaxValue : item.ID).ToList(); + orderedActiveItems = serverSidePlaylist.Where(item => !item.Expired).OrderBy(item => item.ID).ToList(); break; case QueueMode.AllPlayersRoundRobin: @@ -483,7 +482,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { MultiplayerPlaylistItem candidateItem = unprocessedItems .OrderBy(item => perUserCounts[item.OwnerID]) - .ThenBy(item => item.ID == 0 ? int.MaxValue : item.ID) + .ThenBy(item => item.ID) .First(); unprocessedItems.Remove(candidateItem); @@ -504,8 +503,9 @@ namespace osu.Game.Tests.Visual.Multiplayer item.PlaylistOrder = (ushort)i; - // Items which have an ID of 0 are not in the database, so avoid propagating database/hub events for them. - if (item.ID <= 0) + // Items which have an "infinite" ID are not yet in the database, so avoid propagating database/hub events for them. + // See addItem() for when this occurs. + if (item.ID == long.MaxValue) continue; await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); From 16fd7f5a287cf921445f51ae7a587b6ccebe1327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 4 Dec 2021 14:42:01 +0100 Subject: [PATCH 183/419] Simplify slightly redundant assertions --- osu.Game.Tests/Database/RulesetStoreTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index b82dab1874..cc7e8a0c97 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -45,9 +45,9 @@ namespace osu.Game.Tests.Database { var rulesets = new RealmRulesetStore(realmFactory, storage); - Assert.IsTrue(rulesets.AvailableRulesets.First().IsManaged == false); - Assert.IsTrue(rulesets.GetRuleset(0)?.IsManaged == false); - Assert.IsTrue(rulesets.GetRuleset("mania")?.IsManaged == false); + Assert.IsFalse(rulesets.AvailableRulesets.First().IsManaged); + Assert.IsFalse(rulesets.GetRuleset(0)?.IsManaged); + Assert.IsFalse(rulesets.GetRuleset("mania")?.IsManaged); }); } } From 53a6ef22ceb9122faff7a0b538dbf654246b0da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 4 Dec 2021 14:55:35 +0100 Subject: [PATCH 184/419] Add null check to resolve inspection --- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index a68309dae5..8fbaebadfe 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -187,7 +187,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants // Todo: Should use the room's selected item to determine ruleset. var ruleset = rulesets.GetRuleset(0)?.CreateInstance(); - int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank; + int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null; userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); From 054543f58fd8a0ec7641047635d1b8c9d6ff821c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 4 Dec 2021 15:33:02 +0100 Subject: [PATCH 185/419] Revert tournament beatmap panel test change with comment --- .../Components/TestSceneTournamentBeatmapPanel.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs index 7132655535..b678f69b8f 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs @@ -3,20 +3,29 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Tests.Visual; using osu.Game.Tournament.Components; namespace osu.Game.Tournament.Tests.Components { public class TestSceneTournamentBeatmapPanel : TournamentTestScene { + /// + /// Warning: the below API instance is actually the online API, rather than the dummy API provided by the test. + /// It cannot be trivially replaced because setting to causes to no longer be usable. + /// + [Resolved] + private IAPIProvider api { get; set; } + [BackgroundDependencyLoader] private void load() { var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = 1091460 }); req.Success += success; - API.Queue(req); + api.Queue(req); } private void success(APIBeatmap beatmap) From ea6766d940f6e4bf2a1510d375bb09f6d6215023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 4 Dec 2021 16:47:53 +0100 Subject: [PATCH 186/419] Add failing test case --- .../Formats/LegacyScoreDecoderTest.cs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 6e5a546e87..a73ae9dcdb 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -2,14 +2,20 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Globalization; +using System.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko; using osu.Game.Scoring; @@ -21,6 +27,14 @@ namespace osu.Game.Tests.Beatmaps.Formats [TestFixture] public class LegacyScoreDecoderTest { + private CultureInfo originalCulture; + + [SetUp] + public void SetUp() + { + originalCulture = CultureInfo.CurrentCulture; + } + [Test] public void TestDecodeManiaReplay() { @@ -44,6 +58,59 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestCultureInvariance() + { + var ruleset = new OsuRuleset().RulesetInfo; + var scoreInfo = new TestScoreInfo(ruleset); + var beatmap = new TestBeatmap(ruleset); + var score = new Score + { + ScoreInfo = scoreInfo, + Replay = new Replay + { + Frames = new List + { + new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton) + } + } + }; + + // the "se" culture is used here, as it encodes the negative number sign as U+2212 MINUS SIGN, + // rather than the classic ASCII U+002D HYPHEN-MINUS. + CultureInfo.CurrentCulture = new CultureInfo("se"); + + var encodeStream = new MemoryStream(); + + var encoder = new LegacyScoreEncoder(score, beatmap); + encoder.Encode(encodeStream); + + var decodeStream = new MemoryStream(encodeStream.GetBuffer()); + + var decoder = new TestLegacyScoreDecoder(); + var decodedAfterEncode = decoder.Parse(decodeStream); + + Assert.Multiple(() => + { + Assert.That(decodedAfterEncode, Is.Not.Null); + + Assert.That(decodedAfterEncode.ScoreInfo.User.Username, Is.EqualTo(scoreInfo.User.Username)); + Assert.That(decodedAfterEncode.ScoreInfo.BeatmapInfoID, Is.EqualTo(scoreInfo.BeatmapInfoID)); + Assert.That(decodedAfterEncode.ScoreInfo.Ruleset, Is.EqualTo(scoreInfo.Ruleset)); + Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(scoreInfo.TotalScore)); + Assert.That(decodedAfterEncode.ScoreInfo.MaxCombo, Is.EqualTo(scoreInfo.MaxCombo)); + Assert.That(decodedAfterEncode.ScoreInfo.Date, Is.EqualTo(scoreInfo.Date)); + + Assert.That(decodedAfterEncode.Replay.Frames.Count, Is.EqualTo(1)); + }); + } + + [TearDown] + public void TearDown() + { + CultureInfo.CurrentCulture = originalCulture; + } + private class TestLegacyScoreDecoder : LegacyScoreDecoder { private static readonly Dictionary rulesets = new Ruleset[] From f051720fa14256b2f7ad46d9f7aacf40ae157a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 4 Dec 2021 17:02:39 +0100 Subject: [PATCH 187/419] Fix score encoder being dependent on current culture As it turns out, on some cultures, the "negative integer" sign is not encoded using the U+002D HYPHEN-MINUS codepoint. For instance, Swedish uses U+2212 MINUS SIGN instead. This was confusing the legacy decoder, since it is correctly depending on the serialisation being culture-independent. To fix, ensure that the special "end replay" frame, as well as the replay MD5 hash, are generated in a culture-invariant manner. Thankfully the replay MD5 hash is currently being discarded in `LegacyScoreDecoder`, so it changing in future scores should not have any negative effect on lazer operation. --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 7b8cacb35b..3d67aa9558 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -46,7 +46,7 @@ namespace osu.Game.Scoring.Legacy sw.Write(LATEST_VERSION); sw.Write(score.ScoreInfo.BeatmapInfo.MD5Hash); sw.Write(score.ScoreInfo.UserString); - sw.Write($"lazer-{score.ScoreInfo.UserString}-{score.ScoreInfo.Date}".ComputeMD5Hash()); + sw.Write(FormattableString.Invariant($"lazer-{score.ScoreInfo.UserString}-{score.ScoreInfo.Date}").ComputeMD5Hash()); sw.Write((ushort)(score.ScoreInfo.GetCount300() ?? 0)); sw.Write((ushort)(score.ScoreInfo.GetCount100() ?? 0)); sw.Write((ushort)(score.ScoreInfo.GetCount50() ?? 0)); @@ -110,7 +110,9 @@ namespace osu.Game.Scoring.Legacy } } - replayData.AppendFormat(@"{0}|{1}|{2}|{3},", -12345, 0, 0, 0); + // Warning: this is purposefully hardcoded as a string rather than interpolating, as in some cultures the minus sign is not encoded as the standard ASCII U+00C2 codepoint, + // which then would break decoding. + replayData.Append(@"-12345|0|0|0"); return replayData.ToString(); } } From e1897f9998065b499e758503ecff3d11cbaae880 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 5 Dec 2021 01:38:37 +0900 Subject: [PATCH 188/419] Don't debounce `MultiplayerRoomComposite` events This avoids accidental usage which could result in data being lost or ignored (as only the last `user` in a single frame would arrive). This was added specifically to debounce sample playback, but given that it's only debouncing on a single frame (hardly noticeable) I'm not going to add back support for that yet. It should be handled by sample playback concurrency or something more local to the usage. --- .../OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs index 2f75f09a9f..7d2fe44c4e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs @@ -32,9 +32,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } private void invokeOnRoomUpdated() => Scheduler.AddOnce(OnRoomUpdated); - private void invokeUserJoined(MultiplayerRoomUser user) => Scheduler.AddOnce(UserJoined, user); - private void invokeUserKicked(MultiplayerRoomUser user) => Scheduler.AddOnce(UserKicked, user); - private void invokeUserLeft(MultiplayerRoomUser user) => Scheduler.AddOnce(UserLeft, user); + private void invokeUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() => UserJoined(user)); + private void invokeUserKicked(MultiplayerRoomUser user) => Scheduler.Add(() => UserKicked(user)); + private void invokeUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() => UserLeft(user)); private void invokeItemAdded(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemAdded(item)); private void invokeItemRemoved(long item) => Schedule(() => PlaylistItemRemoved(item)); private void invokeItemChanged(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemChanged(item)); From 81215b9f0e7b70c2cfbd31bf0345d64e92fa4fc4 Mon Sep 17 00:00:00 2001 From: ColdVolcano <16726733+ColdVolcano@users.noreply.github.com> Date: Sat, 4 Dec 2021 22:31:55 -0600 Subject: [PATCH 189/419] Use correct effect points when EarlyActivationMilliseconds is not zero --- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 6e4901ab1a..2024d18570 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -111,7 +111,7 @@ namespace osu.Game.Graphics.Containers if (clock == null) return; - double currentTrackTime = clock.CurrentTime; + double currentTrackTime = clock.CurrentTime + EarlyActivationMilliseconds; if (Beatmap.Value.TrackLoaded && Beatmap.Value.BeatmapLoaded) { @@ -132,13 +132,11 @@ namespace osu.Game.Graphics.Containers { // this may be the case where the beat syncing clock has been paused. // we still want to show an idle animation, so use this container's time instead. - currentTrackTime = Clock.CurrentTime; + currentTrackTime = Clock.CurrentTime + EarlyActivationMilliseconds; timingPoint = TimingControlPoint.DEFAULT; effectPoint = EffectControlPoint.DEFAULT; } - currentTrackTime += EarlyActivationMilliseconds; - double beatLength = timingPoint.BeatLength / Divisor; while (beatLength < MinimumBeatLength) From 86c908c657db4771840435887c69964cdbfeb9d3 Mon Sep 17 00:00:00 2001 From: ColdVolcano <16726733+ColdVolcano@users.noreply.github.com> Date: Sun, 5 Dec 2021 03:53:36 -0600 Subject: [PATCH 190/419] Add test coverage --- .../TestSceneBeatSyncedContainer.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index e5bcc08924..ede89c6096 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -135,6 +135,35 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("bpm is default", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 60)); } + [TestCase(true)] + [TestCase(false)] + public void TestEarlyActivationEffectPoint(bool earlyActivating) + { + double earlyActivationMilliseconds = earlyActivating ? 100 : 0; + ControlPoint actualEffectPoint = null; + + AddStep($"set early activation to {earlyActivationMilliseconds}", () => beatContainer.EarlyActivationMilliseconds = earlyActivationMilliseconds); + + AddStep("seek before kiai effect point", () => + { + ControlPoint expectedEffectPoint = Beatmap.Value.Beatmap.ControlPointInfo.EffectPoints.First(ep => ep.KiaiMode); + actualEffectPoint = null; + beatContainer.AllowMistimedEventFiring = false; + + beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => + { + if (Precision.AlmostEquals(gameplayClockContainer.CurrentTime + earlyActivationMilliseconds, expectedEffectPoint.Time, BeatSyncedContainer.MISTIMED_ALLOWANCE)) + actualEffectPoint = effectControlPoint; + }; + + gameplayClockContainer.Seek(expectedEffectPoint.Time - earlyActivationMilliseconds); + }); + + AddUntilStep("wait for effect point", () => actualEffectPoint != null); + + AddAssert("effect has kiai", () => actualEffectPoint != null && ((EffectControlPoint)actualEffectPoint).KiaiMode); + } + private class TestBeatSyncedContainer : BeatSyncedContainer { private const int flash_layer_height = 150; @@ -145,6 +174,12 @@ namespace osu.Game.Tests.Visual.UserInterface set => base.AllowMistimedEventFiring = value; } + public new double EarlyActivationMilliseconds + { + get => base.EarlyActivationMilliseconds; + set => base.EarlyActivationMilliseconds = value; + } + private readonly InfoString timingPointCount; private readonly InfoString currentTimingPoint; private readonly InfoString beatCount; From a07f8c74dc454de38c1ca4b54323e29345747bb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 5 Dec 2021 15:04:44 +0100 Subject: [PATCH 191/419] Add basic structure for composable card dropdown --- .../Visual/Beatmaps/TestSceneBeatmapCard.cs | 10 +- .../Beatmaps/Drawables/Cards/BeatmapCard.cs | 413 +++++++++--------- .../Drawables/Cards/BeatmapCardDropdown.cs | 85 ++++ 3 files changed, 307 insertions(+), 201 deletions(-) create mode 100644 osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs index 5effc1f215..d3ce5028b7 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Graphics.Containers; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -227,7 +228,7 @@ namespace osu.Game.Tests.Visual.Beatmaps new BasicScrollContainer { RelativeSizeAxes = Axes.Both, - Child = new FillFlowContainer + Child = new ReverseChildIDFillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -248,6 +249,11 @@ namespace osu.Game.Tests.Visual.Beatmaps } [Test] - public void TestNormal() => createTestCase(beatmapSetInfo => new BeatmapCard(beatmapSetInfo)); + public void TestNormal() + { + createTestCase(beatmapSetInfo => new BeatmapCard(beatmapSetInfo)); + + AddToggleStep("toggle expanded state", expanded => this.ChildrenOfType().Last().Expanded.Value = expanded); + } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 37c1bacda4..46ee9afb86 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -31,10 +31,12 @@ namespace osu.Game.Beatmaps.Drawables.Cards public class BeatmapCard : OsuClickableContainer { public const float TRANSITION_DURATION = 400; + public const float CORNER_RADIUS = 10; + + public Bindable Expanded { get; } = new BindableBool(); private const float width = 408; private const float height = 100; - private const float corner_radius = 10; private const float icon_area_width = 30; private readonly APIBeatmapSet beatmapSet; @@ -73,242 +75,255 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Width = width; Height = height; - CornerRadius = corner_radius; - Masking = true; FillFlowContainer leftIconArea; GridContainer titleContainer; GridContainer artistContainer; - InternalChildren = new Drawable[] + InternalChild = new BeatmapCardDropdown(height) { - downloadTracker, - rightAreaBackground = new Container + Body = new Container { - RelativeSizeAxes = Axes.Y, - Width = icon_area_width + 2 * corner_radius, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - // workaround for masking artifacts at the top & bottom of card, - // which become especially visible on downloaded beatmaps (when the icon area has a lime background). - Padding = new MarginPadding { Vertical = 1 }, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.White - }, - }, - thumbnail = new BeatmapCardThumbnail(beatmapSet) - { - Name = @"Left (icon) area", - Size = new Vector2(height), - Padding = new MarginPadding { Right = corner_radius }, - Child = leftIconArea = new FillFlowContainer - { - Margin = new MarginPadding(5), - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(1) - } - }, - new Container - { - Name = @"Right (button) area", - Width = 30, - RelativeSizeAxes = Axes.Y, - Origin = Anchor.TopRight, - Anchor = Anchor.TopRight, - Padding = new MarginPadding { Vertical = 17.5f }, - Child = rightAreaButtons = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new BeatmapCardIconButton[] - { - new FavouriteButton(beatmapSet) - { - Current = favouriteState, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - }, - new DownloadButton(beatmapSet) - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - State = { BindTarget = downloadTracker.State } - }, - new GoToBeatmapButton(beatmapSet) - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - State = { BindTarget = downloadTracker.State } - } - } - } - }, - mainContent = new Container - { - Name = @"Main content", - X = height - corner_radius, - Height = height, - CornerRadius = corner_radius, - Masking = true, + RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - mainContentBackground = new BeatmapCardContentBackground(beatmapSet) + downloadTracker, + rightAreaBackground = new Container { - RelativeSizeAxes = Axes.Both, - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding + RelativeSizeAxes = Axes.Y, + Width = icon_area_width + 2 * CORNER_RADIUS, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + // workaround for masking artifacts at the top & bottom of card, + // which become especially visible on downloaded beatmaps (when the icon area has a lime background). + Padding = new MarginPadding { Vertical = 1 }, + Child = new Box { - Horizontal = 10, - Vertical = 4 + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White }, - Direction = FillDirection.Vertical, - Children = new Drawable[] + }, + thumbnail = new BeatmapCardThumbnail(beatmapSet) + { + Name = @"Left (icon) area", + Size = new Vector2(height), + Padding = new MarginPadding { Right = CORNER_RADIUS }, + Child = leftIconArea = new FillFlowContainer { - titleContainer = new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new[] - { - new OsuSpriteText - { - Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title), - Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), - RelativeSizeAxes = Axes.X, - Truncate = true - }, - Empty() - } - } - }, - artistContainer = new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new[] - { - new OsuSpriteText - { - Text = createArtistText(), - Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), - RelativeSizeAxes = Axes.X, - Truncate = true - }, - Empty() - }, - } - }, - new LinkFlowContainer(s => - { - s.Shadow = false; - s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold); - }).With(d => - { - d.AutoSizeAxes = Axes.Both; - d.Margin = new MarginPadding { Top = 2 }; - d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); - d.AddUserLink(beatmapSet.Author); - }), + Margin = new MarginPadding(5), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(1) } }, new Container { - Name = @"Bottom content", - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Padding = new MarginPadding + Name = @"Right (button) area", + Width = 30, + RelativeSizeAxes = Axes.Y, + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + Padding = new MarginPadding { Vertical = 17.5f }, + Child = rightAreaButtons = new Container { - Horizontal = 10, - Vertical = 4 - }, + RelativeSizeAxes = Axes.Both, + Children = new BeatmapCardIconButton[] + { + new FavouriteButton(beatmapSet) + { + Current = favouriteState, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }, + new DownloadButton(beatmapSet) + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + State = { BindTarget = downloadTracker.State } + }, + new GoToBeatmapButton(beatmapSet) + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + State = { BindTarget = downloadTracker.State } + } + } + } + }, + mainContent = new Container + { + Name = @"Main content", + X = height - CORNER_RADIUS, + Height = height, + CornerRadius = CORNER_RADIUS, + Masking = true, Children = new Drawable[] { - idleBottomContent = new FillFlowContainer + mainContentBackground = new BeatmapCardContentBackground(beatmapSet) { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = 10, + Vertical = 4 + }, Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 3), - AlwaysPresent = true, Children = new Drawable[] { - statisticsContainer = new FillFlowContainer + titleContainer = new GridContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Alpha = 0, - AlwaysPresent = true, - ChildrenEnumerable = createStatistics() - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(4, 0), - Children = new Drawable[] + ColumnDimensions = new[] { - new BeatmapSetOnlineStatusPill + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new[] { - AutoSizeAxes = Axes.Both, - Status = beatmapSet.Status, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft - }, - new DifficultySpectrumDisplay(beatmapSet) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - DotSize = new Vector2(6, 12) + new OsuSpriteText + { + Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title), + Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + Truncate = true + }, + Empty() } } - } + }, + artistContainer = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new[] + { + new OsuSpriteText + { + Text = createArtistText(), + Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + Truncate = true + }, + Empty() + }, + } + }, + new LinkFlowContainer(s => + { + s.Shadow = false; + s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold); + }).With(d => + { + d.AutoSizeAxes = Axes.Both; + d.Margin = new MarginPadding { Top = 2 }; + d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); + d.AddUserLink(beatmapSet.Author); + }), } }, - downloadProgressBar = new BeatmapCardDownloadProgressBar + new Container { + Name = @"Bottom content", RelativeSizeAxes = Axes.X, - Height = 6, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - State = { BindTarget = downloadTracker.State }, - Progress = { BindTarget = downloadTracker.Progress } + AutoSizeAxes = Axes.Y, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Padding = new MarginPadding + { + Horizontal = 10, + Vertical = 4 + }, + Children = new Drawable[] + { + idleBottomContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 3), + AlwaysPresent = true, + Children = new Drawable[] + { + statisticsContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Alpha = 0, + AlwaysPresent = true, + ChildrenEnumerable = createStatistics() + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4, 0), + Children = new Drawable[] + { + new BeatmapSetOnlineStatusPill + { + AutoSizeAxes = Axes.Both, + Status = beatmapSet.Status, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + new DifficultySpectrumDisplay(beatmapSet) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + DotSize = new Vector2(6, 12) + } + } + } + } + }, + downloadProgressBar = new BeatmapCardDownloadProgressBar + { + RelativeSizeAxes = Axes.X, + Height = 6, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = { BindTarget = downloadTracker.State }, + Progress = { BindTarget = downloadTracker.Progress } + } + } } } } } - } + }, + Dropdown = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 10, Vertical = 13 }, + Child = new BeatmapCardDifficultyList(beatmapSet) + }, + Expanded = { BindTarget = Expanded } }; if (beatmapSet.HasVideo) @@ -388,7 +403,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { float targetWidth = width - height; if (IsHovered) - targetWidth = targetWidth - icon_area_width + corner_radius; + targetWidth = targetWidth - icon_area_width + CORNER_RADIUS; thumbnail.Dimmed.Value = IsHovered; diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs new file mode 100644 index 0000000000..29f12de947 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs @@ -0,0 +1,85 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Overlays; + +namespace osu.Game.Beatmaps.Drawables.Cards +{ + public class BeatmapCardDropdown : CompositeDrawable + { + public Drawable Body + { + set => bodyContent.Child = value; + } + + public Drawable Dropdown + { + set => dropdownContent.Child = value; + } + + public Bindable Expanded { get; } = new BindableBool(); + + private readonly Box background; + private readonly Container bodyContent; + private readonly Container dropdownContent; + + public BeatmapCardDropdown(float height) + { + RelativeSizeAxes = Axes.X; + Height = height; + + InternalChild = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Masking = true, + + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + bodyContent = new Container + { + RelativeSizeAxes = Axes.X, + Height = height, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Masking = true, + }, + dropdownContent = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = height }, + Alpha = 0 + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + background.Colour = colourProvider.Background2; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Expanded.BindValueChanged(_ => updateState()); + } + + private void updateState() + { + background.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + dropdownContent.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + } + } +} From 3fea8d5e62e709ce46bc480e32dc10b33c6c251f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 5 Dec 2021 15:48:02 +0100 Subject: [PATCH 192/419] Implement visual behaviour of expanded card state --- .../Beatmaps/Drawables/Cards/BeatmapCard.cs | 15 +++++---- .../Drawables/Cards/BeatmapCardDropdown.cs | 33 +++++++++++++++++-- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 46ee9afb86..f0425794be 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -359,7 +359,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards { base.LoadComplete(); - downloadTracker.State.BindValueChanged(_ => updateState(), true); + downloadTracker.State.BindValueChanged(_ => updateState()); + Expanded.BindValueChanged(_ => updateState(), true); FinishTransforms(true); } @@ -401,19 +402,21 @@ namespace osu.Game.Beatmaps.Drawables.Cards private void updateState() { + bool showDetails = IsHovered || Expanded.Value; + float targetWidth = width - height; - if (IsHovered) + if (showDetails) targetWidth = targetWidth - icon_area_width + CORNER_RADIUS; - thumbnail.Dimmed.Value = IsHovered; + thumbnail.Dimmed.Value = showDetails; mainContent.ResizeWidthTo(targetWidth, TRANSITION_DURATION, Easing.OutQuint); - mainContentBackground.Dimmed.Value = IsHovered; + mainContentBackground.Dimmed.Value = showDetails; - statisticsContainer.FadeTo(IsHovered ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); + statisticsContainer.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); rightAreaBackground.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, TRANSITION_DURATION, Easing.OutQuint); - rightAreaButtons.FadeTo(IsHovered ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); + rightAreaButtons.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); foreach (var button in rightAreaButtons) { diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs index 29f12de947..877338ecb0 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs @@ -5,8 +5,10 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Game.Overlays; +using osuTK; namespace osu.Game.Beatmaps.Drawables.Cards { @@ -25,15 +27,17 @@ namespace osu.Game.Beatmaps.Drawables.Cards public Bindable Expanded { get; } = new BindableBool(); private readonly Box background; + private readonly Container content; private readonly Container bodyContent; private readonly Container dropdownContent; + private readonly Container borderContainer; public BeatmapCardDropdown(float height) { RelativeSizeAxes = Axes.X; Height = height; - InternalChild = new Container + InternalChild = content = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -59,6 +63,19 @@ namespace osu.Game.Beatmaps.Drawables.Cards AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Top = height }, Alpha = 0 + }, + borderContainer = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Masking = true, + BorderThickness = 3, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } } } }; @@ -68,18 +85,30 @@ namespace osu.Game.Beatmaps.Drawables.Cards private void load(OverlayColourProvider colourProvider) { background.Colour = colourProvider.Background2; + borderContainer.BorderColour = colourProvider.Highlight1; } protected override void LoadComplete() { base.LoadComplete(); - Expanded.BindValueChanged(_ => updateState()); + Expanded.BindValueChanged(_ => updateState(), true); + FinishTransforms(true); } private void updateState() { background.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); dropdownContent.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + borderContainer.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + content.TweenEdgeEffectTo(new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(0, 2), + Radius = 10, + Colour = Colour4.Black.Opacity(Expanded.Value ? 0.3f : 0f), + Hollow = true, + }, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); } } } From 250e5b47b7b1f9e7c50c0f552529f9b0bc47b4a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 28 Nov 2021 20:25:23 +0100 Subject: [PATCH 193/419] Move "extra info" beatmap card row to separate component --- .../Beatmaps/Drawables/Cards/BeatmapCard.cs | 24 +---------- .../Cards/BeatmapCardExtraInfoRow.cs | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+), 23 deletions(-) create mode 100644 osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index f0425794be..1a05607074 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -276,29 +276,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards AlwaysPresent = true, ChildrenEnumerable = createStatistics() }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(4, 0), - Children = new Drawable[] - { - new BeatmapSetOnlineStatusPill - { - AutoSizeAxes = Axes.Both, - Status = beatmapSet.Status, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft - }, - new DifficultySpectrumDisplay(beatmapSet) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - DotSize = new Vector2(6, 12) - } - } - } + new BeatmapCardExtraInfoRow(beatmapSet) } }, downloadProgressBar = new BeatmapCardDownloadProgressBar diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs new file mode 100644 index 0000000000..c64e5b83d8 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; +using osuTK; + +namespace osu.Game.Beatmaps.Drawables.Cards +{ + public class BeatmapCardExtraInfoRow : CompositeDrawable + { + public BeatmapCardExtraInfoRow(APIBeatmapSet beatmapSet) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4, 0), + Children = new Drawable[] + { + new BeatmapSetOnlineStatusPill + { + AutoSizeAxes = Axes.Both, + Status = beatmapSet.Status, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + new DifficultySpectrumDisplay(beatmapSet) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + DotSize = new Vector2(6, 12) + } + } + }; + } + } +} From e451e43b9022542bc28c6f96e2263c44b27a1213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 5 Dec 2021 16:31:45 +0100 Subject: [PATCH 194/419] Implement input handling behaviour of beatmap card dropdown --- .../Beatmaps/Drawables/Cards/BeatmapCard.cs | 12 ++++- .../Drawables/Cards/BeatmapCardDropdown.cs | 52 +++++++++++++++++-- .../Cards/BeatmapCardExtraInfoRow.cs | 4 +- .../Drawables/Cards/HoverHandlingContainer.cs | 27 ++++++++++ 4 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Beatmaps/Drawables/Cards/HoverHandlingContainer.cs diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 1a05607074..2ab77539b3 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -44,6 +44,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards private readonly BeatmapDownloadTracker downloadTracker; + private BeatmapCardDropdown dropdown = null!; + private BeatmapCardThumbnail thumbnail = null!; private Container rightAreaBackground = null!; @@ -80,7 +82,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards GridContainer titleContainer; GridContainer artistContainer; - InternalChild = new BeatmapCardDropdown(height) + InternalChild = dropdown = new BeatmapCardDropdown(height) { Body = new Container { @@ -277,6 +279,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards ChildrenEnumerable = createStatistics() }, new BeatmapCardExtraInfoRow(beatmapSet) + { + Hovered = _ => + { + dropdown.ScheduleShow(); + return false; + }, + Unhovered = _ => dropdown.ScheduleHide() + } } }, downloadProgressBar = new BeatmapCardDownloadProgressBar diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs index 877338ecb0..ef4ba67ab7 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs @@ -1,12 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Threading; using osu.Game.Overlays; using osuTK; @@ -37,13 +40,13 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.X; Height = height; - InternalChild = content = new Container + InternalChild = content = new HoverHandlingContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, CornerRadius = BeatmapCard.CORNER_RADIUS, Masking = true, - + Unhovered = _ => checkForHide(), Children = new Drawable[] { background = new Box @@ -57,12 +60,18 @@ namespace osu.Game.Beatmaps.Drawables.Cards CornerRadius = BeatmapCard.CORNER_RADIUS, Masking = true, }, - dropdownContent = new Container + dropdownContent = new HoverHandlingContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Top = height }, - Alpha = 0 + Alpha = 0, + Hovered = _ => + { + keep(); + return true; + }, + Unhovered = _ => checkForHide() }, borderContainer = new Container { @@ -95,6 +104,41 @@ namespace osu.Game.Beatmaps.Drawables.Cards FinishTransforms(true); } + private ScheduledDelegate? scheduledExpandedChange; + + public void ScheduleShow() + { + scheduledExpandedChange?.Cancel(); + if (Expanded.Value) + return; + + scheduledExpandedChange = Scheduler.AddDelayed(() => Expanded.Value = true, 100); + } + + public void ScheduleHide() + { + scheduledExpandedChange?.Cancel(); + if (!Expanded.Value) + return; + + scheduledExpandedChange = Scheduler.AddDelayed(() => Expanded.Value = false, 500); + } + + private void checkForHide() + { + if (content.IsHovered || dropdownContent.IsHovered) + return; + + scheduledExpandedChange?.Cancel(); + Expanded.Value = false; + } + + private void keep() + { + scheduledExpandedChange?.Cancel(); + Expanded.Value = true; + } + private void updateState() { background.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs index c64e5b83d8..0a9d98e621 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs @@ -8,14 +8,14 @@ using osuTK; namespace osu.Game.Beatmaps.Drawables.Cards { - public class BeatmapCardExtraInfoRow : CompositeDrawable + public class BeatmapCardExtraInfoRow : HoverHandlingContainer { public BeatmapCardExtraInfoRow(APIBeatmapSet beatmapSet) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - InternalChild = new FillFlowContainer + Child = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Beatmaps/Drawables/Cards/HoverHandlingContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/HoverHandlingContainer.cs new file mode 100644 index 0000000000..1e2c616332 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/HoverHandlingContainer.cs @@ -0,0 +1,27 @@ +// 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 osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; + +namespace osu.Game.Beatmaps.Drawables.Cards +{ + public class HoverHandlingContainer : Container + { + public Func? Hovered { get; set; } + public Action? Unhovered { get; set; } + + protected override bool OnHover(HoverEvent e) => Hovered?.Invoke(e) ?? base.OnHover(e); + + protected override void OnHoverLost(HoverLostEvent e) + { + if (Unhovered != null) + Unhovered?.Invoke(e); + else + base.OnHoverLost(e); + } + } +} From af10223ac4d7841cd5fc7212a09d5498211fbc6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Nov 2021 01:08:45 +0100 Subject: [PATCH 195/419] Add reverse fill flows & depth specs at usage sites for correct Z-ordering --- osu.Game/Overlays/BeatmapListingOverlay.cs | 2 +- osu.Game/Overlays/Profile/ProfileSection.cs | 5 +++-- .../Overlays/Profile/Sections/PaginatedProfileSubsection.cs | 6 ++++-- osu.Game/Overlays/Rankings/SpotlightsLayout.cs | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 49f2f5c211..a454af00c4 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -151,7 +151,7 @@ namespace osu.Game.Overlays } // spawn new children with the contained so we only clear old content at the last moment. - var content = new FillFlowContainer + var content = new ReverseChildIDFillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Overlays/Profile/ProfileSection.cs b/osu.Game/Overlays/Profile/ProfileSection.cs index 6223b32814..fdf3077bf0 100644 --- a/osu.Game/Overlays/Profile/ProfileSection.cs +++ b/osu.Game/Overlays/Profile/ProfileSection.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; @@ -22,7 +23,7 @@ namespace osu.Game.Overlays.Profile public abstract string Identifier { get; } - private readonly FillFlowContainer content; + private readonly FillFlowContainer content; private readonly Box background; private readonly Box underscore; @@ -79,7 +80,7 @@ namespace osu.Game.Overlays.Profile } } }, - content = new FillFlowContainer + content = new ReverseChildIDFillFlowContainer { Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs index 130ae44273..e7053ec4fa 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; +using osu.Game.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; using osuTK; @@ -26,7 +27,7 @@ namespace osu.Game.Overlays.Profile.Sections protected int VisiblePages; protected int ItemsPerPage; - protected FillFlowContainer ItemsContainer { get; private set; } + protected ReverseChildIDFillFlowContainer ItemsContainer { get; private set; } private APIRequest> retrievalRequest; private CancellationTokenSource loadCancellation; @@ -48,11 +49,12 @@ namespace osu.Game.Overlays.Profile.Sections Direction = FillDirection.Vertical, Children = new Drawable[] { - ItemsContainer = new FillFlowContainer + ItemsContainer = new ReverseChildIDFillFlowContainer { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Spacing = new Vector2(0, 2), + Depth = float.MinValue }, moreButton = new ShowMoreButton { diff --git a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs index a37f762532..61d68b7d29 100644 --- a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs +++ b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs @@ -135,7 +135,7 @@ namespace osu.Game.Overlays.Rankings Children = new Drawable[] { new ScoresTable(1, response.Users), - new FillFlowContainer + new ReverseChildIDFillFlowContainer { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, From af35652b8b9f97dc700f19040e161f8d477c2748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 5 Dec 2021 17:16:41 +0100 Subject: [PATCH 196/419] Disable beatmap card expansion on solo spectator screen --- .../Visual/Beatmaps/TestSceneBeatmapCard.cs | 9 +++++++- .../Drawables/Cards/BeatmapCardDropdown.cs | 22 +++++++++++++++---- osu.Game/Screens/Play/SoloSpectator.cs | 5 ++++- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs index d3ce5028b7..f835d21603 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -253,7 +254,13 @@ namespace osu.Game.Tests.Visual.Beatmaps { createTestCase(beatmapSetInfo => new BeatmapCard(beatmapSetInfo)); - AddToggleStep("toggle expanded state", expanded => this.ChildrenOfType().Last().Expanded.Value = expanded); + AddToggleStep("toggle expanded state", expanded => + { + var card = this.ChildrenOfType().Last(); + if (!card.Expanded.Disabled) + card.Expanded.Value = expanded; + }); + AddToggleStep("disable/enable expansion", disabled => this.ChildrenOfType().ForEach(card => card.Expanded.Disabled = disabled)); } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs index ef4ba67ab7..366e5cb0fc 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs @@ -109,23 +109,34 @@ namespace osu.Game.Beatmaps.Drawables.Cards public void ScheduleShow() { scheduledExpandedChange?.Cancel(); - if (Expanded.Value) + if (Expanded.Disabled || Expanded.Value) return; - scheduledExpandedChange = Scheduler.AddDelayed(() => Expanded.Value = true, 100); + scheduledExpandedChange = Scheduler.AddDelayed(() => + { + if (!Expanded.Disabled) + Expanded.Value = true; + }, 100); } public void ScheduleHide() { scheduledExpandedChange?.Cancel(); - if (!Expanded.Value) + if (Expanded.Disabled || !Expanded.Value) return; - scheduledExpandedChange = Scheduler.AddDelayed(() => Expanded.Value = false, 500); + scheduledExpandedChange = Scheduler.AddDelayed(() => + { + if (!Expanded.Disabled) + Expanded.Value = false; + }, 500); } private void checkForHide() { + if (Expanded.Disabled) + return; + if (content.IsHovered || dropdownContent.IsHovered) return; @@ -135,6 +146,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards private void keep() { + if (Expanded.Disabled) + return; + scheduledExpandedChange?.Cancel(); Expanded.Value = true; } diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index 7fea44b3ea..3918dbe8fc 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -228,7 +228,10 @@ namespace osu.Game.Screens.Play onlineBeatmapRequest.Success += beatmapSet => Schedule(() => { this.beatmapSet = beatmapSet; - beatmapPanelContainer.Child = new BeatmapCard(this.beatmapSet); + beatmapPanelContainer.Child = new BeatmapCard(this.beatmapSet) + { + Expanded = { Disabled = true } + }; checkForAutomaticDownload(); }); From 0f743893890029d010b75a42d6472c79a12b69c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 5 Dec 2021 17:35:10 +0100 Subject: [PATCH 197/419] Add scrolling for long difficulty lists in beatmap card --- .../Drawables/Cards/BeatmapCardDropdown.cs | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs index 366e5cb0fc..a24f09a9b5 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs @@ -3,13 +3,17 @@ #nullable enable +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Framework.Threading; +using osu.Framework.Utils; +using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osuTK; @@ -24,7 +28,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards public Drawable Dropdown { - set => dropdownContent.Child = value; + set => dropdownScroll.Child = value; } public Bindable Expanded { get; } = new BindableBool(); @@ -33,6 +37,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards private readonly Container content; private readonly Container bodyContent; private readonly Container dropdownContent; + private readonly OsuScrollContainer dropdownScroll; private readonly Container borderContainer; public BeatmapCardDropdown(float height) @@ -71,7 +76,12 @@ namespace osu.Game.Beatmaps.Drawables.Cards keep(); return true; }, - Unhovered = _ => checkForHide() + Unhovered = _ => checkForHide(), + Child = dropdownScroll = new DropdownScrollContainer + { + RelativeSizeAxes = Axes.X, + ScrollbarVisible = false + } }, borderContainer = new Container { @@ -168,5 +178,54 @@ namespace osu.Game.Beatmaps.Drawables.Cards Hollow = true, }, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); } + + private class DropdownScrollContainer : OsuScrollContainer + { + public DropdownScrollContainer() + { + ScrollbarVisible = false; + } + + protected override void Update() + { + base.Update(); + + Height = Math.Min(Content.DrawHeight, 400); + } + + private bool allowScroll => !Precision.AlmostEquals(DrawSize, Content.DrawSize); + + protected override bool OnDragStart(DragStartEvent e) + { + if (!allowScroll) + return false; + + return base.OnDragStart(e); + } + + protected override void OnDrag(DragEvent e) + { + if (!allowScroll) + return; + + base.OnDrag(e); + } + + protected override void OnDragEnd(DragEndEvent e) + { + if (!allowScroll) + return; + + base.OnDragEnd(e); + } + + protected override bool OnScroll(ScrollEvent e) + { + if (!allowScroll) + return false; + + return base.OnScroll(e); + } + } } } From 91aa38c4f6d60aa243c2af8d806e8606967b3b42 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Dec 2021 12:28:46 +0900 Subject: [PATCH 198/419] Change playlist lookup to fail hard when failing --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index c2b9727309..f8a25e0388 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -452,7 +452,7 @@ namespace osu.Game.Online.Multiplayer Scheduler.Add(() => { // ensure the new selected item is populated immediately. - var playlistItem = APIRoom.Playlist.SingleOrDefault(p => p.ID == newSettings.PlaylistItemId); + var playlistItem = APIRoom.Playlist.Single(p => p.ID == newSettings.PlaylistItemId); if (playlistItem != null) { From 5ff452cc9aa8e47be3a36a15facd7e148bb820dc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Dec 2021 12:29:11 +0900 Subject: [PATCH 199/419] Update success bool to access `Exception` to stop exceptions from firing outwards --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index f8a25e0388..2ddcc09e1b 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -458,7 +458,8 @@ namespace osu.Game.Online.Multiplayer { GetAPIBeatmap(playlistItem.BeatmapID).ContinueWith(b => { - bool success = b.IsCompletedSuccessfully; + // Should be called outside of the `Scheduler` logic (and specifically accessing `Exception`) to suppress an exception from firing outwards. + bool success = b.Exception == null; Scheduler.Add(() => { From ca1f96d2c2ef22d4917a3a60364e949ac546082f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Dec 2021 13:03:11 +0900 Subject: [PATCH 200/419] Reword xmldoc of `MultiplayerPlaylistItem.PlaylistOrder` to better match actual behaviour --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 1c33b79531..cee6d8fe41 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -40,8 +40,12 @@ namespace osu.Game.Online.Rooms public bool Expired { get; set; } /// - /// The order in which this will be played, starting from 0 and increasing for items which will be played later. + /// The order in which this will be played relative to others. + /// Playlist items should be played in increasing order (lower values are played first). /// + /// + /// This is only valid for items which are not . The value for expired items is undefined and should not be used. + /// [Key(8)] public ushort PlaylistOrder { get; set; } From a76cfbea2163ed0869d507ccb9865d5ee55c9cbd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Dec 2021 14:03:24 +0900 Subject: [PATCH 201/419] Add test coverage of incorrect beatmap being used in multiplayer when match started from song select --- .../Multiplayer/TestSceneMultiplayer.cs | 38 +++++++++++++++++++ .../Multiplayer/MultiplayerMatchSubScreen.cs | 14 ++++--- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 4521a7fa0f..843b7a0c51 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -397,6 +397,44 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle); } + [Test] + public void TestPlayStartsWithCorrectBeatmapWhileAtSongSelect() + { + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + }); + + AddStep("Enter song select", () => + { + var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerScreenStack.CurrentScreen).CurrentSubScreen; + + ((MultiplayerMatchSubScreen)currentSubScreen).SelectBeatmap(); + }); + + AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + + AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID); + + AddStep("Select next beatmap", () => InputManager.Key(Key.Down)); + + AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != client.Room?.Playlist.First().BeatmapID); + + AddStep("start match externally", () => client.StartMatch()); + + AddUntilStep("play started", () => multiplayerScreenStack.CurrentScreen is Player); + + AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID); + } + [Test] public void TestLocalPlayDoesNotStartWhileSpectatingWithNoBeatmap() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 16017a1f0e..3a25bd7b06 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -138,11 +138,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { RelativeSizeAxes = Axes.X, Height = 40, - Action = () => - { - if (this.IsCurrentScreen()) - this.Push(new MultiplayerMatchSongSelect(Room)); - }, + Action = SelectBeatmap, Alpha = 0 }, }, @@ -224,6 +220,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } }; + internal void SelectBeatmap() + { + if (!this.IsCurrentScreen()) + return; + + this.Push(new MultiplayerMatchSongSelect(Room)); + } + protected override Drawable CreateFooter() => new MultiplayerMatchFooter { OnReadyClick = onReadyClick, From 0ea7a6908422342c130c35141e0e35aa20e564a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Dec 2021 14:03:51 +0900 Subject: [PATCH 202/419] Ensure user is returned to the `RoomSubScreen` before gameplay is started This covers the scenario where a user may be at the song select screen while another user (the room host) starts the match. This was only made possible with the new queue modes, so is quite a recent regression. --- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 7c5ed3f5cc..184ac2c563 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -319,6 +319,16 @@ namespace osu.Game.Screens.OnlinePlay.Match protected void StartPlay() { + // User may be at song select or otherwise when the host starts gameplay. + // Ensure that they first return to this screen, else global bindables (beatmap etc.) may be in a bad state. + if (!this.IsCurrentScreen()) + { + this.MakeCurrent(); + + Schedule(StartPlay); + return; + } + sampleStart?.Play(); // fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes). From 0fa1a96e9dc508f16c27a0ad7b128c0f8899ccc6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Dec 2021 15:03:17 +0900 Subject: [PATCH 203/419] Wait for beatmap sets to finish loading to avoid test failures Co-authored-by: Dan Balasescu --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 843b7a0c51..2411f39ae3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -420,7 +420,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ((MultiplayerMatchSubScreen)currentSubScreen).SelectBeatmap(); }); - AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true); AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID); From 5be74af8fe58a1a4c859b4af73007e96d4fdd061 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 6 Dec 2021 15:09:06 +0900 Subject: [PATCH 204/419] Update addItem() implementation --- .../Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index f5f5eebe0f..740693ef76 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -430,13 +430,12 @@ namespace osu.Game.Tests.Visual.Multiplayer { Debug.Assert(Room != null); - // Add the item to the list first in order to compute gameplay order. - item.ID = long.MaxValue; - serverSidePlaylist.Add(item); - await updatePlaylistOrder(Room).ConfigureAwait(false); - item.ID = ++lastPlaylistItemId; + + serverSidePlaylist.Add(item); await ((IMultiplayerClient)this).PlaylistItemAdded(item).ConfigureAwait(false); + + await updatePlaylistOrder(Room).ConfigureAwait(false); } private async Task updateCurrentItem(MultiplayerRoom room, bool notify = true) From fae41b21821fa8740004612972385b48d2719be5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 6 Dec 2021 15:17:28 +0900 Subject: [PATCH 205/419] Remove one more piece of code --- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 740693ef76..2510ddb432 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -502,11 +502,6 @@ namespace osu.Game.Tests.Visual.Multiplayer item.PlaylistOrder = (ushort)i; - // Items which have an "infinite" ID are not yet in the database, so avoid propagating database/hub events for them. - // See addItem() for when this occurs. - if (item.ID == long.MaxValue) - continue; - await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); } } From 7a333ffdcc1d8d599d80096fb054462db873c040 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Dec 2021 16:34:15 +0900 Subject: [PATCH 206/419] Add a paired schedule in `SpectatorClient.BeginPlaying` Optimally, I would like to remove the `Schedule` in `EndPlaying`, but it turns out quite a few test are relying on this at very least. Adding a paired schedule ensure that order of operations is correct, at least. --- osu.Game/Online/Spectator/SpectatorClient.cs | 30 +++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 6b95d288c5..4da9bace70 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -136,30 +136,32 @@ namespace osu.Game.Online.Spectator public void BeginPlaying(GameplayState state, Score score) { - Debug.Assert(ThreadSafety.IsUpdateThread); + // This schedule is only here to match the one below in `EndPlaying`. + Schedule(() => + { + if (IsPlaying) + throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing"); - if (IsPlaying) - throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing"); + IsPlaying = true; - IsPlaying = true; + // transfer state at point of beginning play + currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID; + currentState.RulesetID = score.ScoreInfo.RulesetID; + currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); - // transfer state at point of beginning play - currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID; - currentState.RulesetID = score.ScoreInfo.RulesetID; - currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); + currentBeatmap = state.Beatmap; + currentScore = score; - currentBeatmap = state.Beatmap; - currentScore = score; - - BeginPlayingInternal(currentState); + BeginPlayingInternal(currentState); + }); } public void SendFrames(FrameDataBundle data) => lastSend = SendFramesInternal(data); public void EndPlaying() { - // This method is most commonly called via Dispose(), which is asynchronous. - // Todo: This should not be a thing, but requires framework changes. + // This method is most commonly called via Dispose(), which is can be asynchronous (via the AsyncDisposalQueue). + // We probably need to find a better way to handle this... Schedule(() => { if (!IsPlaying) From d58b85b381f6434eb570655c137c4332ff438e0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Dec 2021 16:35:06 +0900 Subject: [PATCH 207/419] Refactor `TestScenSpectatorPlayback` to properly clean up without async disposal --- .../Gameplay/TestSceneSpectatorPlayback.cs | 205 ++++++++++-------- 1 file changed, 109 insertions(+), 96 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 5fbccd54c8..4836cf8ecd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Gameplay private TestReplayRecorder recorder; - private readonly ManualClock manualClock = new ManualClock(); + private ManualClock manualClock; private OsuSpriteText latencyDisplay; @@ -66,113 +66,121 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty()); - [SetUp] - public void SetUp() => Schedule(() => + [SetUpSteps] + public void SetUpSteps() { - replay = new Replay(); + AddStep("Reset recorder state", cleanUpState); - users.BindTo(spectatorClient.PlayingUsers); - users.BindCollectionChanged((obj, args) => + AddStep("Setup containers", () => { - switch (args.Action) + replay = new Replay(); + manualClock = new ManualClock(); + + spectatorClient.OnNewFrames += onNewFrames; + + users.BindTo(spectatorClient.PlayingUsers); + users.BindCollectionChanged((obj, args) => { - case NotifyCollectionChangedAction.Add: - Debug.Assert(args.NewItems != null); - - foreach (int user in args.NewItems) - { - if (user == api.LocalUser.Value.Id) - spectatorClient.WatchUser(user); - } - - break; - - case NotifyCollectionChangedAction.Remove: - Debug.Assert(args.OldItems != null); - - foreach (int user in args.OldItems) - { - if (user == api.LocalUser.Value.Id) - spectatorClient.StopWatchingUser(user); - } - - break; - } - }, true); - - spectatorClient.OnNewFrames += onNewFrames; - - Add(new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] + switch (args.Action) { - recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) + case NotifyCollectionChangedAction.Add: + Debug.Assert(args.NewItems != null); + + foreach (int user in args.NewItems) + { + if (user == api.LocalUser.Value.Id) + spectatorClient.WatchUser(user); + } + + break; + + case NotifyCollectionChangedAction.Remove: + Debug.Assert(args.OldItems != null); + + foreach (int user in args.OldItems) + { + if (user == api.LocalUser.Value.Id) + spectatorClient.StopWatchingUser(user); + } + + break; + } + }, true); + + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] { - Recorder = recorder = new TestReplayRecorder + new Drawable[] { - ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), - }, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { - new Box + Recorder = recorder = new TestReplayRecorder + { + ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), + }, + Child = new Container { - Colour = Color4.Brown, RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Brown, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = "Sending", + Scale = new Vector2(3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new TestInputConsumer() + } }, - new OsuSpriteText - { - Text = "Sending", - Scale = new Vector2(3), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new TestInputConsumer() } }, + new Drawable[] + { + playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) + { + Clock = new FramedClock(manualClock), + ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay) + { + GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos), + }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.DarkBlue, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = "Receiving", + Scale = new Vector2(3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new TestInputConsumer() + } + }, + } + } } }, - new Drawable[] - { - playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) - { - Clock = new FramedClock(manualClock), - ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay) - { - GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos), - }, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = Color4.DarkBlue, - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Text = "Receiving", - Scale = new Vector2(3), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new TestInputConsumer() - } - }, - } - } - } + latencyDisplay = new OsuSpriteText() + }; }); - - Add(latencyDisplay = new OsuSpriteText()); - }); + } private void onNewFrames(int userId, FrameDataBundle frames) { @@ -189,6 +197,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestBasic() { + AddStep("Wait for user input", () => { }); } private double latency = SpectatorClient.TIME_BETWEEN_SENDS; @@ -232,11 +241,15 @@ namespace osu.Game.Tests.Visual.Gameplay [TearDownSteps] public void TearDown() { - AddStep("stop recorder", () => - { - recorder.Expire(); + AddStep("stop recorder", cleanUpState); + } + + private void cleanUpState() + { + // Ensure previous recorder is disposed else it may affect the global playing state of `SpectatorClient`. + recorder?.RemoveAndDisposeImmediately(); + recorder = null; spectatorClient.OnNewFrames -= onNewFrames; - }); } public class TestFramedReplayInputHandler : FramedReplayInputHandler From 50a5f52f92bee93f4fd43e9c08db60363dc8d677 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Dec 2021 16:42:02 +0900 Subject: [PATCH 208/419] Remove duplicated test scene (see `TestSceeneReplayRecorder`) --- .../Gameplay/TestSceneReplayRecording.cs | 228 ------------------ 1 file changed, 228 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs deleted file mode 100644 index 3f7155f1e2..0000000000 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs +++ /dev/null @@ -1,228 +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 osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; -using osu.Framework.Input.StateChanges; -using osu.Game.Beatmaps; -using osu.Game.Graphics.Sprites; -using osu.Game.Replays; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.UI; -using osu.Game.Scoring; -using osu.Game.Screens.Play; -using osu.Game.Tests.Visual.UserInterface; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Tests.Visual.Gameplay -{ - public class TestSceneReplayRecording : OsuTestScene - { - private readonly TestRulesetInputManager playbackManager; - - private readonly TestRulesetInputManager recordingManager; - - [Cached] - private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty()); - - public TestSceneReplayRecording() - { - Replay replay = new Replay(); - - Add(new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] - { - recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) - { - Recorder = new TestReplayRecorder(new Score - { - Replay = replay, - ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo } - }) - { - ScreenSpaceToGamefield = pos => recordingManager?.ToLocalSpace(pos) ?? Vector2.Zero, - }, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = Color4.Brown, - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Text = "Recording", - Scale = new Vector2(3), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new TestConsumer() - } - }, - } - }, - new Drawable[] - { - playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) - { - ReplayInputHandler = new TestFramedReplayInputHandler(replay) - { - GamefieldToScreenSpace = pos => playbackManager?.ToScreenSpace(pos) ?? Vector2.Zero, - }, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = Color4.DarkBlue, - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Text = "Playback", - Scale = new Vector2(3), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new TestConsumer() - } - }, - } - } - } - }); - } - - protected override void Update() - { - base.Update(); - - playbackManager.ReplayInputHandler.SetFrameFromTime(Time.Current - 500); - } - } - - public class TestFramedReplayInputHandler : FramedReplayInputHandler - { - public TestFramedReplayInputHandler(Replay replay) - : base(replay) - { - } - - public override void CollectPendingInputs(List inputs) - { - inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) }); - inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() }); - } - } - - public class TestConsumer : CompositeDrawable, IKeyBindingHandler - { - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent.ReceivePositionalInputAt(screenSpacePos); - - private readonly Box box; - - public TestConsumer() - { - Size = new Vector2(30); - - Origin = Anchor.Centre; - - InternalChildren = new Drawable[] - { - box = new Box - { - Colour = Color4.Black, - RelativeSizeAxes = Axes.Both, - }, - }; - } - - protected override bool OnMouseMove(MouseMoveEvent e) - { - Position = e.MousePosition; - return base.OnMouseMove(e); - } - - public bool OnPressed(KeyBindingPressEvent e) - { - if (e.Repeat) - return false; - - box.Colour = Color4.White; - return true; - } - - public void OnReleased(KeyBindingReleaseEvent e) - { - box.Colour = Color4.Black; - } - } - - public class TestRulesetInputManager : RulesetInputManager - { - public TestRulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) - : base(ruleset, variant, unique) - { - } - - protected override KeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) - => new TestKeyBindingContainer(); - - internal class TestKeyBindingContainer : KeyBindingContainer - { - public override IEnumerable DefaultKeyBindings => new[] - { - new KeyBinding(InputKey.MouseLeft, TestAction.Down), - }; - } - } - - public class TestReplayFrame : ReplayFrame - { - public Vector2 Position; - - public List Actions = new List(); - - public TestReplayFrame(double time, Vector2 position, params TestAction[] actions) - : base(time) - { - Position = position; - Actions.AddRange(actions); - } - } - - public enum TestAction - { - Down, - } - - internal class TestReplayRecorder : ReplayRecorder - { - public TestReplayRecorder(Score target) - : base(target) - { - } - - protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame) => - new TestReplayFrame(Time.Current, mousePosition, actions.ToArray()); - } -} From 0d3d22d3e1fdee1d40a375dc4941cb0014457ce0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Dec 2021 16:42:24 +0900 Subject: [PATCH 209/419] Update `TestSceneReplayReocorder` to be safer about disposal --- .../Gameplay/TestSceneReplayRecorder.cs | 136 ++++++++++-------- 1 file changed, 74 insertions(+), 62 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index dcc193669b..e6361a15d7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -43,83 +43,88 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty()); - [SetUp] - public void SetUp() => Schedule(() => + [SetUpSteps] + public void SetUpSteps() { - replay = new Replay(); + AddStep("Reset recorder state", cleanUpState); - Add(new GridContainer + AddStep("Setup containers", () => { - RelativeSizeAxes = Axes.Both, - Content = new[] + replay = new Replay(); + + Add(new GridContainer { - new Drawable[] + RelativeSizeAxes = Axes.Both, + Content = new[] { - recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) + new Drawable[] { - Recorder = recorder = new TestReplayRecorder(new Score + recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { - Replay = replay, - ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo } - }) - { - ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), - }, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Recorder = recorder = new TestReplayRecorder(new Score { - new Box + Replay = replay, + ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo } + }) + { + ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), + }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Colour = Color4.Brown, - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Text = "Recording", - Scale = new Vector2(3), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new TestInputConsumer() - } - }, - } - }, - new Drawable[] - { - playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) + new Box + { + Colour = Color4.Brown, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = "Recording", + Scale = new Vector2(3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new TestInputConsumer() + } + }, + } + }, + new Drawable[] { - ReplayInputHandler = new TestFramedReplayInputHandler(replay) + playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { - GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos), - }, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + ReplayInputHandler = new TestFramedReplayInputHandler(replay) { - new Box + GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos), + }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Colour = Color4.DarkBlue, - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Text = "Playback", - Scale = new Vector2(3), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new TestInputConsumer() - } - }, + new Box + { + Colour = Color4.DarkBlue, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = "Playback", + Scale = new Vector2(3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new TestInputConsumer() + } + }, + } } } - } + }); }); - }); + } [Test] public void TestBasic() @@ -184,7 +189,14 @@ namespace osu.Game.Tests.Visual.Gameplay [TearDownSteps] public void TearDown() { - AddStep("stop recorder", () => recorder.Expire()); + AddStep("stop recorder", cleanUpState); + } + + private void cleanUpState() + { + // Ensure previous recorder is disposed else it may affect the global playing state of `SpectatorClient`. + recorder?.RemoveAndDisposeImmediately(); + recorder = null; } public class TestFramedReplayInputHandler : FramedReplayInputHandler From 568364b604cbffcfe3ed94bf9dbb9a014130cd98 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 6 Dec 2021 17:38:57 +0900 Subject: [PATCH 210/419] Fix indentation --- .../Visual/Gameplay/TestSceneSpectatorPlayback.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 4836cf8ecd..f7e9a1fe16 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -246,10 +246,10 @@ namespace osu.Game.Tests.Visual.Gameplay private void cleanUpState() { - // Ensure previous recorder is disposed else it may affect the global playing state of `SpectatorClient`. - recorder?.RemoveAndDisposeImmediately(); - recorder = null; - spectatorClient.OnNewFrames -= onNewFrames; + // Ensure previous recorder is disposed else it may affect the global playing state of `SpectatorClient`. + recorder?.RemoveAndDisposeImmediately(); + recorder = null; + spectatorClient.OnNewFrames -= onNewFrames; } public class TestFramedReplayInputHandler : FramedReplayInputHandler From 3e27859e4f7f3b02ddc7551b9e7033bedf4cc1f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Dec 2021 22:47:07 +0900 Subject: [PATCH 211/419] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 17b5cb67e9..ce23d36faa 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 53a3337c9d..788a5113ad 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index f3dc163a67..2aa9821e8f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From d5cdb1bb8786d41a896ce4b57293a144d9998bbf Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 7 Dec 2021 00:01:07 +0900 Subject: [PATCH 212/419] Update test multiplayer client implementation --- .../Multiplayer/TestMultiplayerClient.cs | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 2510ddb432..b3ea5bdc4a 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -464,32 +464,28 @@ namespace osu.Game.Tests.Visual.Multiplayer break; case QueueMode.AllPlayersRoundRobin: - orderedActiveItems = new List(); + var itemsByPriority = new List<(MultiplayerPlaylistItem item, int priority)>(); - // Todo: This could probably be more efficient, likely at the cost of increased complexity. - // Number of "expired" or "used" items per player. - Dictionary perUserCounts = serverSidePlaylist - .GroupBy(item => item.OwnerID) - .ToDictionary(group => group.Key, group => group.Count(item => item.Expired)); - - // We'll run a simulation over all items which are not expired ("unprocessed"). Expired items will not have their ordering updated. - List unprocessedItems = serverSidePlaylist.Where(item => !item.Expired).ToList(); - - // In every iteration of the simulation, pick the first available item from the user with the lowest number of items in the queue to add to the result set. - // If multiple users have the same number of items in the queue, then the item with the lowest ID is chosen. - while (unprocessedItems.Count > 0) + // Assign a priority for items from each user, starting from 0 and increasing in order which the user added the items. + foreach (var group in room.Playlist.Where(item => !item.Expired).OrderBy(item => item.ID).GroupBy(item => item.OwnerID)) { - MultiplayerPlaylistItem candidateItem = unprocessedItems - .OrderBy(item => perUserCounts[item.OwnerID]) - .ThenBy(item => item.ID) - .First(); - - unprocessedItems.Remove(candidateItem); - orderedActiveItems.Add(candidateItem); - - perUserCounts[candidateItem.OwnerID]++; + int priority = 0; + itemsByPriority.AddRange(group.Select(item => (item, priority++))); } + orderedActiveItems = itemsByPriority + // Order by each user's priority. + .OrderBy(i => i.priority) + // Many users will have the same priority of items, so attempt to break the tie by maintaining previous ordering. + // Suppose there are two users: User1 and User2. User1 adds two items, and then User2 adds a third. If the previous order is not maintained, + // then after playing the first item by User1, their second item will become priority=0 and jump to the front of the queue (because it was added first). + .ThenBy(i => i.item.PlaylistOrder) + // If there are still ties (normally shouldn't happen), break ties by making items added earlier go first. + // This could happen if e.g. the item orders get reset. + .ThenBy(i => i.item.ID) + .Select(i => i.item) + .ToList(); + break; } From 4278a320e46042c47de929500b559ed079dc20b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 Dec 2021 20:12:02 +0100 Subject: [PATCH 213/419] Fix skin setting resetting every launch The reason this was happening was an unfortunate oversight in the migration logic. The code that was attempting to parse the skin settings as `int` was firing regardless of whether a skin migration from EF to realm had already occurred. If it had occurred, the skin setting would contain a GUID rather than an integer, and therefore fail to parse, and therefore implicitly fallback to a EF skin ID of 0 which would be the default skin. Fix by not running the setting migrating logic at all when there are no EF skins to migrate. --- osu.Game/Database/EFToRealmMigrator.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index 3790dc8ae9..b79a982460 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -35,6 +35,16 @@ namespace osu.Game.Database private void migrateSkins(DatabaseWriteUsage db) { + // can be removed 20220530. + var existingSkins = db.Context.SkinInfo + .Include(s => s.Files) + .ThenInclude(f => f.FileInfo) + .ToList(); + + // previous entries in EF are removed post migration. + if (!existingSkins.Any()) + return; + var userSkinChoice = config.GetBindable(OsuSetting.Skin); int.TryParse(userSkinChoice.Value, out int userSkinInt); @@ -49,16 +59,6 @@ namespace osu.Game.Database break; } - // migrate ruleset settings. can be removed 20220530. - var existingSkins = db.Context.SkinInfo - .Include(s => s.Files) - .ThenInclude(f => f.FileInfo) - .ToList(); - - // previous entries in EF are removed post migration. - if (!existingSkins.Any()) - return; - using (var realm = realmContextFactory.CreateContext()) using (var transaction = realm.BeginWrite()) { From ccfc361626415fe5993fae093bcfb35abcf980e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 Dec 2021 20:49:29 +0100 Subject: [PATCH 214/419] Apply naming suggestions --- osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs | 12 ++++++------ ...eatmapCardDropdown.cs => BeatmapCardContent.cs} | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) rename osu.Game/Beatmaps/Drawables/Cards/{BeatmapCardDropdown.cs => BeatmapCardContent.cs} (94%) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 2ab77539b3..9031d6df1a 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -44,7 +44,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards private readonly BeatmapDownloadTracker downloadTracker; - private BeatmapCardDropdown dropdown = null!; + private BeatmapCardContent content = null!; private BeatmapCardThumbnail thumbnail = null!; @@ -82,9 +82,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards GridContainer titleContainer; GridContainer artistContainer; - InternalChild = dropdown = new BeatmapCardDropdown(height) + InternalChild = content = new BeatmapCardContent(height) { - Body = new Container + MainContent = new Container { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -282,10 +282,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Hovered = _ => { - dropdown.ScheduleShow(); + content.ScheduleShow(); return false; }, - Unhovered = _ => dropdown.ScheduleHide() + Unhovered = _ => content.ScheduleHide() } } }, @@ -304,7 +304,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards } } }, - Dropdown = new Container + ExpandedContent = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs similarity index 94% rename from osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs rename to osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs index a24f09a9b5..7f94d0e3b7 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDropdown.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs @@ -19,14 +19,14 @@ using osuTK; namespace osu.Game.Beatmaps.Drawables.Cards { - public class BeatmapCardDropdown : CompositeDrawable + public class BeatmapCardContent : CompositeDrawable { - public Drawable Body + public Drawable MainContent { set => bodyContent.Child = value; } - public Drawable Dropdown + public Drawable ExpandedContent { set => dropdownScroll.Child = value; } @@ -40,7 +40,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards private readonly OsuScrollContainer dropdownScroll; private readonly Container borderContainer; - public BeatmapCardDropdown(float height) + public BeatmapCardContent(float height) { RelativeSizeAxes = Axes.X; Height = height; @@ -77,7 +77,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards return true; }, Unhovered = _ => checkForHide(), - Child = dropdownScroll = new DropdownScrollContainer + Child = dropdownScroll = new ExpandedContentScrollContainer { RelativeSizeAxes = Axes.X, ScrollbarVisible = false @@ -179,9 +179,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards }, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); } - private class DropdownScrollContainer : OsuScrollContainer + private class ExpandedContentScrollContainer : OsuScrollContainer { - public DropdownScrollContainer() + public ExpandedContentScrollContainer() { ScrollbarVisible = false; } From 82ed8eae6bfa4f42ecb274f1138f414ad42f7d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 Dec 2021 20:52:06 +0100 Subject: [PATCH 215/419] Ensure hover handling container always calls base on hover events --- .../Drawables/Cards/HoverHandlingContainer.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/HoverHandlingContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/HoverHandlingContainer.cs index 1e2c616332..fe37616755 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/HoverHandlingContainer.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/HoverHandlingContainer.cs @@ -14,14 +14,16 @@ namespace osu.Game.Beatmaps.Drawables.Cards public Func? Hovered { get; set; } public Action? Unhovered { get; set; } - protected override bool OnHover(HoverEvent e) => Hovered?.Invoke(e) ?? base.OnHover(e); + protected override bool OnHover(HoverEvent e) + { + bool handledByBase = base.OnHover(e); + return Hovered?.Invoke(e) ?? handledByBase; + } protected override void OnHoverLost(HoverLostEvent e) { - if (Unhovered != null) - Unhovered?.Invoke(e); - else - base.OnHoverLost(e); + base.OnHoverLost(e); + Unhovered?.Invoke(e); } } } From 999bba439f98bb31741acbfadd4b7a834be3a7a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 Dec 2021 21:00:26 +0100 Subject: [PATCH 216/419] Clarify usages of reverse child ID flow with inline comments --- osu.Game/Overlays/BeatmapListingOverlay.cs | 1 + osu.Game/Overlays/Profile/ProfileSection.cs | 2 ++ .../Overlays/Profile/Sections/PaginatedProfileSubsection.cs | 3 +++ osu.Game/Overlays/Rankings/SpotlightsLayout.cs | 1 + 4 files changed, 7 insertions(+) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index a454af00c4..6b27dbf847 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -151,6 +151,7 @@ namespace osu.Game.Overlays } // spawn new children with the contained so we only clear old content at the last moment. + // reverse ID flow is required for correct Z-ordering of the cards' expandable content (last card should be front-most). var content = new ReverseChildIDFillFlowContainer { RelativeSizeAxes = Axes.X, diff --git a/osu.Game/Overlays/Profile/ProfileSection.cs b/osu.Game/Overlays/Profile/ProfileSection.cs index fdf3077bf0..fc6fce0d8e 100644 --- a/osu.Game/Overlays/Profile/ProfileSection.cs +++ b/osu.Game/Overlays/Profile/ProfileSection.cs @@ -80,6 +80,8 @@ namespace osu.Game.Overlays.Profile } } }, + // reverse ID flow is required for correct Z-ordering of the content (last item should be front-most). + // particularly important in BeatmapsSection, as it uses beatmap cards, which have expandable overhanging content. content = new ReverseChildIDFillFlowContainer { Direction = FillDirection.Vertical, diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs index e7053ec4fa..9dcbf6142d 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs @@ -49,11 +49,14 @@ namespace osu.Game.Overlays.Profile.Sections Direction = FillDirection.Vertical, Children = new Drawable[] { + // reverse ID flow is required for correct Z-ordering of the items (last item should be front-most). + // particularly important in PaginatedBeatmapContainer, as it uses beatmap cards, which have expandable overhanging content. ItemsContainer = new ReverseChildIDFillFlowContainer { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Spacing = new Vector2(0, 2), + // ensure the container and its contents are in front of the "more" button. Depth = float.MinValue }, moreButton = new ShowMoreButton diff --git a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs index 61d68b7d29..bcfc2499b9 100644 --- a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs +++ b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs @@ -135,6 +135,7 @@ namespace osu.Game.Overlays.Rankings Children = new Drawable[] { new ScoresTable(1, response.Users), + // reverse ID flow is required for correct Z-ordering of the cards' expandable content (last card should be front-most). new ReverseChildIDFillFlowContainer { AutoSizeAxes = Axes.Y, From 0a6c221de4dda5d3f41c504974f204b79be4f0ab Mon Sep 17 00:00:00 2001 From: tbrose Date: Mon, 6 Dec 2021 22:07:47 +0100 Subject: [PATCH 217/419] Adds tests for checkContainsUsername function of MessageNotifier component --- .../Online/Chat/MessageNotifierTest.cs | 66 +++++++++++++++++++ osu.Game/Online/Chat/MessageNotifier.cs | 2 +- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Online/Chat/MessageNotifierTest.cs diff --git a/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs b/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs new file mode 100644 index 0000000000..9fed72d249 --- /dev/null +++ b/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs @@ -0,0 +1,66 @@ +// 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.Online.Chat; + +namespace osu.Game.Tests.Online.Chat +{ + [TestFixture] + public class TestCheckUsername + { + [Test] + public void TestContainsUsernameMidlinePositive() + { + Assert.IsTrue(MessageNotifier.checkContainsUsername("This is a test message", "Test")); + } + + [Test] + public void TestContainsUsernameStartOfLinePositive() + { + Assert.IsTrue(MessageNotifier.checkContainsUsername("Test message", "Test")); + } + + [Test] + public void TestContainsUsernameEndOfLinePositive() + { + Assert.IsTrue(MessageNotifier.checkContainsUsername("This is a test", "Test")); + } + + [Test] + public void TestContainsUsernameMidlineNegative() + { + Assert.IsFalse(MessageNotifier.checkContainsUsername("This is a testmessage for notifications", "Test")); + } + + [Test] + public void TestContainsUsernameStartOfLineNegative() + { + Assert.IsFalse(MessageNotifier.checkContainsUsername("Testmessage", "Test")); + } + + [Test] + public void TestContainsUsernameEndOfLineNegative() + { + Assert.IsFalse(MessageNotifier.checkContainsUsername("This is a notificationtest", "Test")); + } + + [Test] + public void TestContainsUsernameBetweenInterpunction() + { + Assert.IsTrue(MessageNotifier.checkContainsUsername("Hello 'test'-message", "Test")); + } + + [Test] + public void TestContainsUsernameUnicode() + { + Assert.IsTrue(MessageNotifier.checkContainsUsername("Test \u0460\u0460 message", "\u0460\u0460")); + } + + [Test] + public void TestContainsUsernameUnicodeNegative() + { + Assert.IsFalse(MessageNotifier.checkContainsUsername("Test ha\u0460\u0460o message", "\u0460\u0460")); + } + } +} diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index ca6317566f..69b3b18b3e 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -129,7 +129,7 @@ namespace osu.Game.Online.Chat /// Checks if contains . /// This will match against the case where underscores are used instead of spaces (which is how osu-stable handles usernames with spaces). ///
- private static bool checkContainsUsername(string message, string username) => message.Contains(username, StringComparison.OrdinalIgnoreCase) || message.Contains(username.Replace(' ', '_'), StringComparison.OrdinalIgnoreCase); + public static bool checkContainsUsername(string message, string username) => message.Contains(username, StringComparison.OrdinalIgnoreCase) || message.Contains(username.Replace(' ', '_'), StringComparison.OrdinalIgnoreCase); public class PrivateMessageNotification : OpenChannelNotification { From 39594b7362747b329bb9486ab7b53019d15671fe Mon Sep 17 00:00:00 2001 From: tbrose Date: Mon, 6 Dec 2021 23:32:21 +0100 Subject: [PATCH 218/419] Fixes detection of mentioning of user falsely detects messages where the username is coincidentally contained in words of a message. --- osu.Game/Online/Chat/MessageNotifier.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 69b3b18b3e..aca9df0600 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; +using System.Text.RegularExpressions; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -126,10 +127,14 @@ namespace osu.Game.Online.Chat } /// - /// Checks if contains . + /// Checks if mentions . /// This will match against the case where underscores are used instead of spaces (which is how osu-stable handles usernames with spaces). /// - public static bool checkContainsUsername(string message, string username) => message.Contains(username, StringComparison.OrdinalIgnoreCase) || message.Contains(username.Replace(' ', '_'), StringComparison.OrdinalIgnoreCase); + public static bool checkContainsUsername(string message, string username) { + string fullName = Regex.Escape(username); + string underscoreName = Regex.Escape(username.Replace(' ', '_')); + return new Regex($"\\b({fullName}|{underscoreName})\\b", RegexOptions.IgnoreCase).Matches(message).Count > 0; + } public class PrivateMessageNotification : OpenChannelNotification { From f02e44d552495823285f23f5e9af8138b70ec2eb Mon Sep 17 00:00:00 2001 From: tbrose Date: Tue, 7 Dec 2021 01:38:37 +0100 Subject: [PATCH 219/419] Fixes not matching coding style --- osu.Game.Tests/Online/Chat/MessageNotifierTest.cs | 2 +- osu.Game/Online/Chat/MessageNotifier.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs b/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs index 9fed72d249..81baaef2d2 100644 --- a/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs +++ b/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs @@ -7,7 +7,7 @@ using osu.Game.Online.Chat; namespace osu.Game.Tests.Online.Chat { [TestFixture] - public class TestCheckUsername + public class MessageNotifierTest { [Test] public void TestContainsUsernameMidlinePositive() diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index aca9df0600..fbbf8fff38 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -130,7 +130,8 @@ namespace osu.Game.Online.Chat /// Checks if mentions . /// This will match against the case where underscores are used instead of spaces (which is how osu-stable handles usernames with spaces). ///
- public static bool checkContainsUsername(string message, string username) { + public static bool checkContainsUsername(string message, string username) + { string fullName = Regex.Escape(username); string underscoreName = Regex.Escape(username.Replace(' ', '_')); return new Regex($"\\b({fullName}|{underscoreName})\\b", RegexOptions.IgnoreCase).Matches(message).Count > 0; From 7a0d4fca17fd22f72a721bb18f20ab387dbb260d Mon Sep 17 00:00:00 2001 From: tbrose Date: Tue, 7 Dec 2021 01:41:21 +0100 Subject: [PATCH 220/419] Fixes using Matches+Count instead of IsMatch negatively affecting performance --- osu.Game/Online/Chat/MessageNotifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index fbbf8fff38..4ae4e64855 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -134,7 +134,7 @@ namespace osu.Game.Online.Chat { string fullName = Regex.Escape(username); string underscoreName = Regex.Escape(username.Replace(' ', '_')); - return new Regex($"\\b({fullName}|{underscoreName})\\b", RegexOptions.IgnoreCase).Matches(message).Count > 0; + return new Regex($"\\b({fullName}|{underscoreName})\\b", RegexOptions.IgnoreCase).IsMatch(message); } public class PrivateMessageNotification : OpenChannelNotification From 0a8c4f4cecfa4e483f97decf6a6383102aadf228 Mon Sep 17 00:00:00 2001 From: tbrose Date: Tue, 7 Dec 2021 01:55:45 +0100 Subject: [PATCH 221/419] Adds test cases for usernames with special characters --- osu.Game.Tests/Online/Chat/MessageNotifierTest.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs b/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs index 81baaef2d2..25dbdb9ea2 100644 --- a/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs +++ b/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs @@ -62,5 +62,17 @@ namespace osu.Game.Tests.Online.Chat { Assert.IsFalse(MessageNotifier.checkContainsUsername("Test ha\u0460\u0460o message", "\u0460\u0460")); } + + [Test] + public void TestContainsUsernameSpecialCharactersPositive() + { + Assert.IsTrue(MessageNotifier.checkContainsUsername("Test [#^-^#] message", "[#^-^#]")); + } + + [Test] + public void TestContainsUsernameSpecialCharactersNegative() + { + Assert.IsFalse(MessageNotifier.checkContainsUsername("Test pad[#^-^#]oru message", "[#^-^#]")); + } } } From 974987550ff53d9a628f6d6dbf6d9c265147e677 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 7 Dec 2021 04:01:53 +0300 Subject: [PATCH 222/419] Move API request response size log to correct logging target --- osu.Game/Online/API/APIRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index efb0b102d0..91148c177f 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -42,7 +42,7 @@ namespace osu.Game.Online.API if (WebRequest != null) { Response = ((OsuJsonWebRequest)WebRequest).ResponseObject; - Logger.Log($"{GetType()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes"); + Logger.Log($"{GetType()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network); } } From b6d47a41f411959a4fe2105f7fa25eff76f46dc2 Mon Sep 17 00:00:00 2001 From: tbrose Date: Tue, 7 Dec 2021 02:14:40 +0100 Subject: [PATCH 223/419] Adjusted RegEx pattern to also take special characters into account --- osu.Game/Online/Chat/MessageNotifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 4ae4e64855..883f6a7284 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -134,7 +134,7 @@ namespace osu.Game.Online.Chat { string fullName = Regex.Escape(username); string underscoreName = Regex.Escape(username.Replace(' ', '_')); - return new Regex($"\\b({fullName}|{underscoreName})\\b", RegexOptions.IgnoreCase).IsMatch(message); + return new Regex($"(^|^\\W|\\W|\\w\\W)({fullName}|{underscoreName})($|$\\W|\\W|\\w\\W)", RegexOptions.IgnoreCase).IsMatch(message); } public class PrivateMessageNotification : OpenChannelNotification From be86ca582cc114788c28e44fcb487e99e1c32dbf Mon Sep 17 00:00:00 2001 From: tbrose Date: Tue, 7 Dec 2021 02:34:35 +0100 Subject: [PATCH 224/419] Adds test cases for at-sign and colon adjacent to the username --- osu.Game.Tests/Online/Chat/MessageNotifierTest.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs b/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs index 25dbdb9ea2..b885299d1f 100644 --- a/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs +++ b/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs @@ -74,5 +74,17 @@ namespace osu.Game.Tests.Online.Chat { Assert.IsFalse(MessageNotifier.checkContainsUsername("Test pad[#^-^#]oru message", "[#^-^#]")); } + + [Test] + public void TestContainsUsernameAtSign() + { + Assert.IsTrue(MessageNotifier.checkContainsUsername("@username hi", "username")); + } + + [Test] + public void TestContainsUsernameColon() + { + Assert.IsTrue(MessageNotifier.checkContainsUsername("username: hi", "username")); + } } } From 882223b27f684cbc9c02b4365f4f3728862a808a Mon Sep 17 00:00:00 2001 From: tbrose Date: Tue, 7 Dec 2021 02:38:10 +0100 Subject: [PATCH 225/419] Using static call and verbatim symbol and optimizes regex pattern for username check --- osu.Game/Online/Chat/MessageNotifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 883f6a7284..db7c5e47f5 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -134,7 +134,7 @@ namespace osu.Game.Online.Chat { string fullName = Regex.Escape(username); string underscoreName = Regex.Escape(username.Replace(' ', '_')); - return new Regex($"(^|^\\W|\\W|\\w\\W)({fullName}|{underscoreName})($|$\\W|\\W|\\w\\W)", RegexOptions.IgnoreCase).IsMatch(message); + return Regex.IsMatch(message, $@"(^|\W)({fullName}|{underscoreName})($|\W)", RegexOptions.IgnoreCase); } public class PrivateMessageNotification : OpenChannelNotification From f3e9fb76fcf98e7da97687b64846878af8bfc0b7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Dec 2021 13:32:35 +0900 Subject: [PATCH 226/419] Add the ability to pass a `CancellationToken` through `DifficultyCalculator.CalculateAll` Was weirdly missing from this one method. --- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 01b4150030..6b6ea6fed5 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -120,14 +120,14 @@ namespace osu.Game.Rulesets.Difficulty /// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap. ///
/// A collection of structures describing the difficulty of the beatmap for each mod combination. - public IEnumerable CalculateAll() + public IEnumerable CalculateAll(CancellationToken cancellationToken = default) { foreach (var combination in CreateDifficultyAdjustmentModCombinations()) { if (combination is MultiMod multi) - yield return Calculate(multi.Mods); + yield return Calculate(multi.Mods, cancellationToken); else - yield return Calculate(combination.Yield()); + yield return Calculate(combination.Yield(), cancellationToken); } } From cfa712473d471862634941f559d5bbd614a15086 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Dec 2021 13:33:41 +0900 Subject: [PATCH 227/419] Use default timeout in `GetPlayableBeatmap` when provided `CancellationToken` is `default` --- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 6b6ea6fed5..6b61dd3efb 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -145,7 +145,11 @@ namespace osu.Game.Rulesets.Difficulty { playableMods = mods.Select(m => m.DeepClone()).ToArray(); - Beatmap = beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken); + // Only pass through the cancellation token if it's non-default. + // This allows for the default timeout to be applied for playable beatmap construction. + Beatmap = cancellationToken == default + ? beatmap.GetPlayableBeatmap(ruleset, playableMods) + : beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken); var track = new TrackVirtual(10000); playableMods.OfType().ForEach(m => m.ApplyToTrack(track)); From dea7f2308c00f673ce16d42a51d346d2d5dead5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Dec 2021 17:24:03 +0900 Subject: [PATCH 228/419] Fix participant panels potentially keeping a reference to an old user If a user leave and then rejoins a multiplayer match while another user is not at the lobby screen, there is a potential the `ParticipantPanel` tracking the user will not correctly be recreated to reference the new instance of the `MultiplayerUser`. This happens because the `OnRoomUpdated` call is scheduled, which means it is not running in the background, coupled with the local logic that relies on `IEquatable(MultiplayerRoomUser)` (which in turn falls back to a UserID comparison). Changing this to a reference comparison is the easiest way to resolve this. Whether we change the `IEquatable` implementation is up for discussion. Closes https://github.com/ppy/osu/issues/15970. --- .../Multiplayer/Participants/ParticipantsList.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs index 2ad64e115e..d36c556fac 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs @@ -77,7 +77,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants else { // Remove panels for users no longer in the room. - panels.RemoveAll(p => !Room.Users.Contains(p.User)); + foreach (var p in panels) + { + // Note that we *must* use reference equality here, as this call is scheduled and a user may have left and joined since it was last run. + if (Room.Users.All(u => !ReferenceEquals(p.User, u))) + p.Expire(); + } // Add panels for all users new to the room. foreach (var user in Room.Users.Except(panels.Select(p => p.User))) From 9978caab127c3ce89ee7a77433ac2685c7f1fe55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Dec 2021 18:37:30 +0900 Subject: [PATCH 229/419] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index ce23d36faa..0c922c09ac 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 788a5113ad..adb25f46fe 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 2aa9821e8f..db5d9af865 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From 5ffe702dd67e5e9444cd1f8fd9a19771011a70f3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 7 Dec 2021 18:53:08 +0900 Subject: [PATCH 230/419] Add match type and queue mode to multiplayer room panels --- .../Multiplayer/TestSceneDrawableRoom.cs | 35 +++++++++++++ .../Lounge/Components/DrawableRoom.cs | 31 ++++++------ .../Lounge/Components/MatchTypePill.cs | 50 +++++++++++++++++++ .../Lounge/Components/QueueModePill.cs | 50 +++++++++++++++++++ .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 13 +++++ .../OnlinePlay/Match/DrawableMatchRoom.cs | 8 +++ 6 files changed, 173 insertions(+), 14 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Lounge/Components/MatchTypePill.cs create mode 100644 osu.Game/Screens/OnlinePlay/Lounge/Components/QueueModePill.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs index 2c28a1752e..423822cbe4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs @@ -11,12 +11,14 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Overlays; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Tests.Beatmaps; using osuTK; @@ -172,6 +174,39 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha)); } + [Test] + public void TestMultiplayerRooms() + { + AddStep("create rooms", () => Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Children = new[] + { + new DrawableMatchRoom(new Room + { + Name = { Value = "A host-only room" }, + QueueMode = { Value = QueueMode.HostOnly }, + Type = { Value = MatchType.HeadToHead } + }), + new DrawableMatchRoom(new Room + { + Name = { Value = "An all-players, team-versus room" }, + QueueMode = { Value = QueueMode.AllPlayers }, + Type = { Value = MatchType.TeamVersus } + }), + new DrawableMatchRoom(new Room + { + Name = { Value = "A round-robin room" }, + QueueMode = { Value = QueueMode.AllPlayersRoundRobin }, + Type = { Value = MatchType.HeadToHead } + }), + } + }); + } + private DrawableRoom createLoungeRoom(Room room) { room.Host.Value ??= new APIUser { Username = "peppy", Id = 2 }; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 0502c4abe6..ba3866d734 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.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.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -184,20 +185,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(5), - Children = new Drawable[] - { - new PlaylistCountPill - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - new StarRatingRangeDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Scale = new Vector2(0.8f) - } - } + ChildrenEnumerable = CreateBottomDetails() } } }, @@ -287,6 +275,21 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components protected virtual Drawable CreateBackground() => new OnlinePlayBackgroundSprite(); + protected virtual IEnumerable CreateBottomDetails() => new Drawable[] + { + new PlaylistCountPill + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new StarRatingRangeDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(0.8f) + } + }; + private class RoomNameText : OsuSpriteText { [Resolved(typeof(Room), nameof(Online.Rooms.Room.Name))] diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/MatchTypePill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/MatchTypePill.cs new file mode 100644 index 0000000000..d104ede8f7 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/MatchTypePill.cs @@ -0,0 +1,50 @@ +// 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.Extensions; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.Rooms; + +namespace osu.Game.Screens.OnlinePlay.Lounge.Components +{ + public class MatchTypePill : OnlinePlayComposite + { + private OsuTextFlowContainer textFlow; + + public MatchTypePill() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new PillContainer + { + Child = textFlow = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12)) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Type.BindValueChanged(onMatchTypeChanged, true); + } + + private void onMatchTypeChanged(ValueChangedEvent type) + { + textFlow.Clear(); + textFlow.AddText(type.NewValue.GetLocalisableDescription()); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/QueueModePill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/QueueModePill.cs new file mode 100644 index 0000000000..7501f0237b --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/QueueModePill.cs @@ -0,0 +1,50 @@ +// 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.Extensions; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Screens.OnlinePlay.Lounge.Components +{ + public class QueueModePill : OnlinePlayComposite + { + private OsuTextFlowContainer textFlow; + + public QueueModePill() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new PillContainer + { + Child = textFlow = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12)) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + QueueMode.BindValueChanged(onQueueModeChanged, true); + } + + private void onQueueModeChanged(ValueChangedEvent mode) + { + textFlow.Clear(); + textFlow.AddText(mode.NewValue.GetLocalisableDescription()); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 0d2b2249ef..529c056f4f 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -177,6 +178,18 @@ namespace osu.Game.Screens.OnlinePlay.Lounge return true; } + protected override IEnumerable CreateBottomDetails() + { + if (Room.Type.Value == MatchType.Playlists) + return base.CreateBottomDetails(); + + return new Drawable[] + { + new MatchTypePill(), + new QueueModePill(), + }.Concat(base.CreateBottomDetails()); + } + public class PasswordEntryPopover : OsuPopover { private readonly Room room; diff --git a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs index a7b907c7d2..ed457967e7 100644 --- a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -67,6 +69,12 @@ namespace osu.Game.Screens.OnlinePlay.Match protected override Drawable CreateBackground() => background = new BackgroundSprite(); + protected override IEnumerable CreateBottomDetails() => new Drawable[] + { + new MatchTypePill(), + new QueueModePill(), + }.Concat(base.CreateBottomDetails()); + private class BackgroundSprite : UpdateableBeatmapBackgroundSprite { protected override double LoadDelay => 0; From 4683193f098fcf640eb80514ecee3280514cc104 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 7 Dec 2021 20:36:39 +0900 Subject: [PATCH 231/419] Move implementation to base class --- .../Lounge/Components/DrawableRoom.cs | 38 +++++++++++++------ .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 13 ------- .../OnlinePlay/Match/DrawableMatchRoom.cs | 8 ---- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index ba3866d734..a87f21630c 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -275,20 +275,36 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components protected virtual Drawable CreateBackground() => new OnlinePlayBackgroundSprite(); - protected virtual IEnumerable CreateBottomDetails() => new Drawable[] + protected virtual IEnumerable CreateBottomDetails() { - new PlaylistCountPill + var pills = new List(); + + if (Room.Type.Value != MatchType.Playlists) { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - new StarRatingRangeDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Scale = new Vector2(0.8f) + pills.AddRange(new OnlinePlayComposite[] + { + new MatchTypePill(), + new QueueModePill(), + }); } - }; + + pills.AddRange(new Drawable[] + { + new PlaylistCountPill + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new StarRatingRangeDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(0.8f) + } + }); + + return pills; + } private class RoomNameText : OsuSpriteText { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 529c056f4f..0d2b2249ef 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -178,18 +177,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge return true; } - protected override IEnumerable CreateBottomDetails() - { - if (Room.Type.Value == MatchType.Playlists) - return base.CreateBottomDetails(); - - return new Drawable[] - { - new MatchTypePill(), - new QueueModePill(), - }.Concat(base.CreateBottomDetails()); - } - public class PasswordEntryPopover : OsuPopover { private readonly Room room; diff --git a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs index ed457967e7..a7b907c7d2 100644 --- a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -69,12 +67,6 @@ namespace osu.Game.Screens.OnlinePlay.Match protected override Drawable CreateBackground() => background = new BackgroundSprite(); - protected override IEnumerable CreateBottomDetails() => new Drawable[] - { - new MatchTypePill(), - new QueueModePill(), - }.Concat(base.CreateBottomDetails()); - private class BackgroundSprite : UpdateableBeatmapBackgroundSprite { protected override double LoadDelay => 0; From ded86282c1b44574b8f1fe1711766640b77b17b4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 7 Dec 2021 23:14:35 +0900 Subject: [PATCH 232/419] Rename + better documentation --- .../Preprocessing/OsuDifficultyHitObject.cs | 37 +++++++++++++------ .../Difficulty/Skills/Aim.cs | 16 ++++---- .../Difficulty/Skills/Flashlight.cs | 2 +- .../Difficulty/Skills/Speed.cs | 2 +- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index f1f246359a..8ecba7375c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -24,22 +24,35 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing public readonly double StrainTime; /// - /// Normalised distance from the end position of the previous to the start position of this . + /// Normalised distance from the "lazy" end position of the previous to the start position of this . + /// + /// The "lazy" end position is the position at which the cursor ends up if the previous hitobject is followed with as minimal movement as possible (i.e. on the edge of slider follow circles). + /// /// - public double JumpDistance { get; private set; } + public double LazyJumpDistance { get; private set; } /// - /// Normalised minimum distance from the end position of the previous to the start position of this . + /// Normalised shortest distance to consider for a jump between the previous and this . /// /// - /// This is bounded by , but may be smaller if a more natural path is able to be taken through a preceding slider. + /// This is bounded from above by , and is smaller than the former if a more natural path is able to be taken through the previous . /// - public double MovementDistance { get; private set; } + /// + /// Suppose a linear slider - circle pattern. + ///
+ /// Following the slider lazily (see: ) will result in underestimating the true end position of the slider as being closer towards the start position. + /// As a result, overestimates the jump distance because the player is able to take a more natural path by following through the slider to its end, + /// such that the jump is felt as only starting from the slider's true end position. + ///
+ /// Now consider a slider - circle pattern where the circle is stacked along the path inside the slider. + /// In this case, the lazy end position correctly estimates the true end position of the slider and provides the more natural movement path. + ///
+ public double MinimumJumpDistance { get; private set; } /// - /// The time taken to travel through , with a minimum value of 25ms. + /// The time taken to travel through , with a minimum value of 25ms. /// - public double MovementTime { get; private set; } + public double MinimumJumpTime { get; private set; } /// /// Normalised distance between the start and end position of this . @@ -96,14 +109,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing Vector2 lastCursorPosition = getEndCursorPosition(lastObject); - JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length; - MovementTime = StrainTime; - MovementDistance = JumpDistance; + LazyJumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length; + MinimumJumpTime = StrainTime; + MinimumJumpDistance = LazyJumpDistance; if (lastObject is Slider lastSlider) { double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time); - MovementTime = Math.Max(StrainTime - lastTravelTime, min_delta_time); + MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, min_delta_time); // // We'll try to better approximate the real movements a player will take in patterns following on from sliders. Consider the following slider-to-object patterns: @@ -127,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing // float tailJumpDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor; - MovementDistance = Math.Max(0, Math.Min(JumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius)); + MinimumJumpDistance = Math.Max(0, Math.Min(LazyJumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius)); } if (lastLastObject != null && !(lastLastObject is Spinner)) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index d2a1083f29..a6301aed6d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -44,24 +44,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills var osuLastLastObj = (OsuDifficultyHitObject)Previous[1]; // Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle. - double currVelocity = osuCurrObj.JumpDistance / osuCurrObj.StrainTime; + double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime; // But if the last object is a slider, then we extend the travel velocity through the slider into the current object. if (osuLastObj.BaseObject is Slider && withSliders) { double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; // calculate the slider velocity from slider head to slider end. - double movementVelocity = osuCurrObj.MovementDistance / osuCurrObj.MovementTime; // calculate the movement velocity from slider end to current object + double movementVelocity = osuCurrObj.MinimumJumpDistance / osuCurrObj.MinimumJumpTime; // calculate the movement velocity from slider end to current object currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity. } // As above, do the same for the previous hitobject. - double prevVelocity = osuLastObj.JumpDistance / osuLastObj.StrainTime; + double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime; if (osuLastLastObj.BaseObject is Slider && withSliders) { double travelVelocity = osuLastLastObj.TravelDistance / osuLastLastObj.TravelTime; - double movementVelocity = osuLastObj.MovementDistance / osuLastObj.MovementTime; + double movementVelocity = osuLastObj.MinimumJumpDistance / osuLastObj.MinimumJumpTime; prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity); } @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern. * Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4 - * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.JumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter). + * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter). } // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute. @@ -107,8 +107,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (Math.Max(prevVelocity, currVelocity) != 0) { // We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities. - prevVelocity = (osuLastObj.JumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.StrainTime; - currVelocity = (osuCurrObj.JumpDistance + osuLastObj.TravelDistance) / osuCurrObj.StrainTime; + prevVelocity = (osuLastObj.LazyJumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.StrainTime; + currVelocity = (osuCurrObj.LazyJumpDistance + osuLastObj.TravelDistance) / osuCurrObj.StrainTime; // Scale with ratio of difference compared to 0.5 * max dist. double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2); @@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills // Reward for % distance slowed down compared to previous, paying attention to not award overlap double nonOverlapVelocityBuff = Math.Abs(prevVelocity - currVelocity) // do not award overlap - * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, Math.Min(osuCurrObj.JumpDistance, osuLastObj.JumpDistance) / 100)), 2); + * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, Math.Min(osuCurrObj.LazyJumpDistance, osuLastObj.LazyJumpDistance) / 100)), 2); // Choose the largest bonus, multiplied by ratio. velocityChangeBonus = Math.Max(overlapVelocityBuff, nonOverlapVelocityBuff) * distRatio; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 466f0556ab..44ba0e2057 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills smallDistNerf = Math.Min(1.0, jumpDistance / 75.0); // We also want to nerf stacks so that only the first object of the stack is accounted for. - double stackNerf = Math.Min(1.0, (osuPrevious.JumpDistance / scalingFactor) / 25.0); + double stackNerf = Math.Min(1.0, (osuPrevious.LazyJumpDistance / scalingFactor) / 25.0); result += Math.Pow(0.8, i) * stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index b53d287ee6..75a9b13bdf 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -155,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); double travelDistance = osuPrevObj?.TravelDistance ?? 0; - double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.JumpDistance); + double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.LazyJumpDistance); return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime; } From 7e236c3a4140f0023628dfe00e5b8966f2425827 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 7 Dec 2021 23:36:48 +0900 Subject: [PATCH 233/419] Remove unused dependency --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index de82c463b3..c291bddeeb 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -25,7 +25,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osu.Game.Online.Chat; -using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays.BeatmapSet; using osu.Game.Rulesets; @@ -72,9 +71,6 @@ namespace osu.Game.Screens.OnlinePlay [Resolved] private BeatmapLookupCache beatmapLookupCache { get; set; } - [Resolved] - private MultiplayerClient multiplayerClient { get; set; } - private PanelBackground panelBackground; private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both }; From 25a0505c97df49a825d874551afc69daf0972c47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Dec 2021 14:26:11 +0900 Subject: [PATCH 234/419] Scale card when expanding to better distinguish hovered card from other cards in listing --- osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs | 14 +++++++++++++- .../Beatmaps/Drawables/Cards/BeatmapCardContent.cs | 3 +++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 9031d6df1a..d93ac841ab 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -285,7 +285,13 @@ namespace osu.Game.Beatmaps.Drawables.Cards content.ScheduleShow(); return false; }, - Unhovered = _ => content.ScheduleHide() + Unhovered = _ => + { + // This hide should only trigger if the expanded content has not shown yet. + // ie. if the user has not shown intent to want to see it (quickly moved over the info row area). + if (!Expanded.Value) + content.ScheduleHide(); + } } } }, @@ -360,6 +366,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards protected override void OnHoverLost(HoverLostEvent e) { + content.ScheduleHide(); + updateState(); base.OnHoverLost(e); } @@ -398,6 +406,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards thumbnail.Dimmed.Value = showDetails; + // Scale value is intentionally chosen to fit in the spacing of listing displays, as to not overlap horizontally with adjacent cards. + // This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left. + content.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint); + mainContent.ResizeWidthTo(targetWidth, TRANSITION_DURATION, Easing.OutQuint); mainContentBackground.Dimmed.Value = showDetails; diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs index 7f94d0e3b7..681f09c658 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs @@ -45,6 +45,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.X; Height = height; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + InternalChild = content = new HoverHandlingContainer { RelativeSizeAxes = Axes.X, From 0b0ff361542521ea066c06d2bad6955c2905873a Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Tue, 7 Dec 2021 20:06:22 +0100 Subject: [PATCH 235/419] Allow only number characters parseable by `int.TryParse` char.IsNumber() is too broad, allowing full width and other numbers. --- osu.Game/Overlays/Settings/SettingsNumberBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index cbe9f7fc64..0a4949f8b6 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -67,7 +67,7 @@ namespace osu.Game.Overlays.Settings private class OutlinedNumberBox : OutlinedTextBox { - protected override bool CanAddCharacter(char character) => char.IsNumber(character); + protected override bool CanAddCharacter(char character) => character >= '0' && character <= '9'; public new void NotifyInputError() => base.NotifyInputError(); } From e9694dc74e0514dae02e0ba65538ac2f8e76f613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 7 Dec 2021 20:40:10 +0100 Subject: [PATCH 236/419] Wait for match type changes in team versus test --- osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 981989c28a..ccce26ad31 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddAssert("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus); + AddUntilStep("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus); AddAssert("user state arrived", () => client.Room?.Users.FirstOrDefault()?.MatchState is TeamVersusUserState); } @@ -162,13 +162,13 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddAssert("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead); + AddUntilStep("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead); AddUntilStep("team displays are not displaying teams", () => multiplayerScreenStack.ChildrenOfType().All(d => d.DisplayedTeam == null)); AddStep("change to team vs", () => client.ChangeSettings(matchType: MatchType.TeamVersus)); - AddAssert("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus); + AddUntilStep("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus); AddUntilStep("team displays are displaying teams", () => multiplayerScreenStack.ChildrenOfType().All(d => d.DisplayedTeam != null)); } From 10dd64e07c89165ee5b437f13c36cab09c9eec2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 7 Dec 2021 21:00:25 +0100 Subject: [PATCH 237/419] Fix being able to paste objects while composer is loading Would lead to exceptions due to modification of `Beatmap.HitObjects` during its enumeration by `DrawableRuleset`, which was happening as an async load via `EditorScreenWithTimeline.CreateMainContent()`. --- osu.Game/Screens/Edit/Compose/ComposeScreen.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 3b02d42b41..9386538a78 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -83,7 +83,9 @@ namespace osu.Game.Screens.Edit.Compose { base.LoadComplete(); EditorBeatmap.SelectedHitObjects.BindCollectionChanged((_, __) => updateClipboardActionAvailability()); - clipboard.BindValueChanged(_ => updateClipboardActionAvailability(), true); + clipboard.BindValueChanged(_ => updateClipboardActionAvailability()); + composer.OnLoadComplete += _ => updateClipboardActionAvailability(); + updateClipboardActionAvailability(); } #region Clipboard operations @@ -131,7 +133,7 @@ namespace osu.Game.Screens.Edit.Compose private void updateClipboardActionAvailability() { CanCut.Value = CanCopy.Value = EditorBeatmap.SelectedHitObjects.Any(); - CanPaste.Value = !string.IsNullOrEmpty(clipboard.Value); + CanPaste.Value = composer.IsLoaded && !string.IsNullOrEmpty(clipboard.Value); } private string formatSelectionAsString() From 7720a1b69b87f85b09440d1f5e62a4b8ab14fda7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 7 Dec 2021 21:10:45 +0100 Subject: [PATCH 238/419] Fix test to wait for drawable ruleset load before attempting paste --- osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs index c81a1abfbc..c23db5e440 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs @@ -10,6 +10,7 @@ using osu.Game.Beatmaps; using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit; using osu.Game.Tests.Beatmaps.IO; @@ -89,6 +90,7 @@ namespace osu.Game.Tests.Visual.Editing confirmEditingBeatmap(() => targetDifficulty); AddAssert("no objects selected", () => !EditorBeatmap.SelectedHitObjects.Any()); + AddUntilStep("wait for drawable ruleset", () => Editor.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); AddStep("paste object", () => Editor.Paste()); if (sameRuleset) From fea3b9d7a9c1a0cc87e3473dec0af49c25007fb1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 8 Dec 2021 04:43:01 +0300 Subject: [PATCH 239/419] Disable minimum frame durations on osu! for iOS --- osu.Game.Rulesets.Catch.Tests.iOS/Info.plist | 2 ++ osu.Game.Rulesets.Mania.Tests.iOS/Info.plist | 2 ++ osu.Game.Rulesets.Osu.Tests.iOS/Info.plist | 2 ++ osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist | 2 ++ osu.Game.Tests.iOS/Info.plist | 2 ++ osu.iOS/Info.plist | 2 ++ 6 files changed, 12 insertions(+) diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist index 5115746cbb..3ba1886d98 100644 --- a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist @@ -32,5 +32,7 @@ XSAppIconAssets Assets.xcassets/AppIcon.appiconset + CADisableMinimumFrameDurationOnPhone + diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist index 8780204d5b..09ed2dd007 100644 --- a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist @@ -32,5 +32,7 @@ XSAppIconAssets Assets.xcassets/AppIcon.appiconset + CADisableMinimumFrameDurationOnPhone + diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist index f79215cf54..dd032ef1c1 100644 --- a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist @@ -32,5 +32,7 @@ XSAppIconAssets Assets.xcassets/AppIcon.appiconset + CADisableMinimumFrameDurationOnPhone + diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist index 5fe822946a..ac658cd14e 100644 --- a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist @@ -32,5 +32,7 @@ XSAppIconAssets Assets.xcassets/AppIcon.appiconset + CADisableMinimumFrameDurationOnPhone + diff --git a/osu.Game.Tests.iOS/Info.plist b/osu.Game.Tests.iOS/Info.plist index 98a4223116..1a89345bc5 100644 --- a/osu.Game.Tests.iOS/Info.plist +++ b/osu.Game.Tests.iOS/Info.plist @@ -32,5 +32,7 @@ XSAppIconAssets Assets.xcassets/AppIcon.appiconset + CADisableMinimumFrameDurationOnPhone + diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 249474b1d7..2592f909ce 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -33,6 +33,8 @@ UIStatusBarHidden + CADisableMinimumFrameDurationOnPhone + NSCameraUsageDescription We don't really use the camera. NSMicrophoneUsageDescription From a969fe3ef811a64c0e05a70342645782dd9d4f77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Dec 2021 13:37:46 +0900 Subject: [PATCH 240/419] Add test coverage showing intended UX of user's volume levels being retained when unmuting --- osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 06eaa726c9..958d617d63 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -251,7 +251,12 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestMutedNotificationMuteButton() { - addVolumeSteps("mute button", () => volumeOverlay.IsMuted.Value = true, () => !volumeOverlay.IsMuted.Value); + addVolumeSteps("mute button", () => + { + // Importantly, in the case the volume is muted but the user has a volume level set, it should be retained. + audioManager.VolumeTrack.Value = 0.5f; + volumeOverlay.IsMuted.Value = true; + }, () => !volumeOverlay.IsMuted.Value && audioManager.VolumeTrack.Value == 0.5f); } /// From 0775053a18a711c8eb9fff57a26ff80c8b98caca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Dec 2021 13:38:13 +0900 Subject: [PATCH 241/419] Fix the unmute notification potentially overwriting user's volume levels unnecessarily I've also changed the cutoffs to 5% rather than zero, as this seems like a saner method of showing this dialog. With levels 5% or less, the game is basically inaudible. Arguably, the cutoff can be increased to 10%. --- osu.Game/Screens/Play/PlayerLoader.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 57db411571..dfc3c2b61d 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -468,12 +468,14 @@ namespace osu.Game.Screens.Play private int restartCount; + private const double volume_requirement = 0.05; + private void showMuteWarningIfNeeded() { if (!muteWarningShownOnce.Value) { // Checks if the notification has not been shown yet and also if master volume is muted, track/music volume is muted or if the whole game is muted. - if (volumeOverlay?.IsMuted.Value == true || audioManager.Volume.Value <= audioManager.Volume.MinValue || audioManager.VolumeTrack.Value <= audioManager.VolumeTrack.MinValue) + if (volumeOverlay?.IsMuted.Value == true || audioManager.Volume.Value <= volume_requirement || audioManager.VolumeTrack.Value <= volume_requirement) { notificationOverlay?.Post(new MutedNotification()); muteWarningShownOnce.Value = true; @@ -487,7 +489,7 @@ namespace osu.Game.Screens.Play public MutedNotification() { - Text = "Your music volume is set to 0%! Click here to restore it."; + Text = "Your game volume is too low to hear anything! Click here to restore it."; } [BackgroundDependencyLoader] @@ -501,8 +503,12 @@ namespace osu.Game.Screens.Play notificationOverlay.Hide(); volumeOverlay.IsMuted.Value = false; - audioManager.Volume.SetDefault(); - audioManager.VolumeTrack.SetDefault(); + + // Check values before resetting, as the user may have only had mute enabled, in which case we might not need to adjust volumes. + if (audioManager.Volume.Value < volume_requirement) + audioManager.Volume.SetDefault(); + if (audioManager.VolumeTrack.Value < volume_requirement) + audioManager.VolumeTrack.SetDefault(); return true; }; From 7c0f7b1baac4fbc16791f2208605f7a77ec78920 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 8 Dec 2021 14:57:21 +0900 Subject: [PATCH 242/419] Use "x" for cursor position in diagrams --- .../Preprocessing/OsuDifficultyHitObject.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 8ecba7375c..1b6914bfaf 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -121,20 +121,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing // // We'll try to better approximate the real movements a player will take in patterns following on from sliders. Consider the following slider-to-object patterns: // - // 1. <======o==> + // 1. <======x==> // | / - // o + // x // - // 2. <======o==>---o + // 2. <======x==>---x // |______| // - // Where "<==>" represents a slider, and "o" represents where the cursor needs to be for either hitobject (for a slider, this is the lazy cursor position). + // Where "<==>" represents a slider, and "x" represents where the cursor needs to be for either hitobject (for a slider, this is the lazy cursor position). // - // The pattern (o--o) has distance JumpDistance. - // The pattern (>--o) is a new distance we'll call "tailJumpDistance". + // The pattern (x--x) has distance JumpDistance. + // The pattern (>--x) is a new distance we'll call "tailJumpDistance". // - // Case (1) is an anti-flow pattern, where players will cut the slider short in order to move to the next object. The most natural jump pattern is (o--o). - // Case (2) is a flow pattern, where players will follow the slider through to its visual extent. The most natural jump pattern is (>--o). + // Case (1) is an anti-flow pattern, where players will cut the slider short in order to move to the next object. The most natural jump pattern is (x--x). + // Case (2) is a flow pattern, where players will follow the slider through to its visual extent. The most natural jump pattern is (>--x). // // A lenience is applied by assuming that the player jumps the minimum of these two distances in all cases. // From 6ec3f41839a6c8a3a6cca8c75162d7c100336605 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Dec 2021 14:56:38 +0900 Subject: [PATCH 243/419] Fix `LegacyComboCounter` not handling non-default anchor/origin specifications correctly --- .../Screens/Play/HUD/LegacyComboCounter.cs | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 4859f1b977..f1078c5d55 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -67,22 +67,32 @@ namespace osu.Game.Screens.Play.HUD Scale = new Vector2(1.2f); - InternalChild = counterContainer = new Container + InternalChildren = new[] { - AutoSizeAxes = Axes.Both, - AlwaysPresent = true, - Children = new[] + popOutCount = new LegacySpriteText(LegacyFont.Combo) { - popOutCount = new LegacySpriteText(LegacyFont.Combo) + Alpha = 0, + Margin = new MarginPadding(0.05f), + Blending = BlendingParameters.Additive, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + BypassAutoSizeAxes = Axes.Both, + }, + counterContainer = new Container + { + AutoSizeAxes = Axes.Both, + AlwaysPresent = true, + Children = new[] { - Alpha = 0, - Margin = new MarginPadding(0.05f), - Blending = BlendingParameters.Additive, - }, - displayedCountSpriteText = new LegacySpriteText(LegacyFont.Combo) - { - Alpha = 0, - }, + displayedCountSpriteText = new LegacySpriteText(LegacyFont.Combo) + { + // Initial text and AlwaysPresent allow the counter to have a size before it first displays a combo. + // This is useful for display in the skin editor. + Text = formatCount(0), + AlwaysPresent = true, + Alpha = 0, + }, + } } }; } @@ -121,13 +131,6 @@ namespace osu.Game.Screens.Play.HUD ((IHasText)displayedCountSpriteText).Text = formatCount(Current.Value); - counterContainer.Anchor = Anchor; - counterContainer.Origin = Origin; - displayedCountSpriteText.Anchor = Anchor; - displayedCountSpriteText.Origin = Origin; - popOutCount.Anchor = Anchor; - popOutCount.Origin = Origin; - Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true); } From 814f072767f52ceb576eb8d7ecfdeceb14340ae7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 8 Dec 2021 15:17:56 +0900 Subject: [PATCH 244/419] Use new LazyJumpDistance terminology in documentation --- .../Difficulty/Preprocessing/OsuDifficultyHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 1b6914bfaf..6c354cfa99 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -130,7 +130,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing // // Where "<==>" represents a slider, and "x" represents where the cursor needs to be for either hitobject (for a slider, this is the lazy cursor position). // - // The pattern (x--x) has distance JumpDistance. + // The pattern (x--x) has distance LazyJumpDistance. // The pattern (>--x) is a new distance we'll call "tailJumpDistance". // // Case (1) is an anti-flow pattern, where players will cut the slider short in order to move to the next object. The most natural jump pattern is (x--x). From 872e0884c0a0997a16c23fcb1b394688409d5803 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Dec 2021 15:22:02 +0900 Subject: [PATCH 245/419] Fix the local user's rank not showing on multiplayer/playlist results screen Applying the simple solution for now. Not sure how this will evolve over time, but seems sane enough. --- osu.Game/Screens/Play/SubmittingPlayer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 76411c8c6b..c07cfa9c4d 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -157,6 +157,8 @@ namespace osu.Game.Screens.Play request.Success += s => { score.ScoreInfo.OnlineScoreID = s.ID; + score.ScoreInfo.Position = s.Position; + scoreSubmissionSource.SetResult(true); }; From 11104124f1f923fa4c1f6bf06ac2441a7ac3cc0c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 8 Dec 2021 15:52:59 +0900 Subject: [PATCH 246/419] Restructure doc for easier readability --- .../Preprocessing/OsuDifficultyHitObject.cs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 6c354cfa99..2c81b42e6c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -119,24 +119,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, min_delta_time); // - // We'll try to better approximate the real movements a player will take in patterns following on from sliders. Consider the following slider-to-object patterns: + // There are two types of slider-to-object patterns to consider in order to better approximate the real movements a player will take. // - // 1. <======x==> - // | / - // x + // 1. The anti-flow pattern, where players cut the slider short in order to move to the next hitobject. // - // 2. <======x==>---x - // |______| + // <======o==> ← slider + // | ← most natural jump path + // o ← a follow-up hitcircle // - // Where "<==>" represents a slider, and "x" represents where the cursor needs to be for either hitobject (for a slider, this is the lazy cursor position). + // In this case the most natural jump path (o--o) is approximated by LazyJumpDistance. // - // The pattern (x--x) has distance LazyJumpDistance. - // The pattern (>--x) is a new distance we'll call "tailJumpDistance". + // 2. The flow pattern, where players follow through the slider to its visual extent into the next hitobject. // - // Case (1) is an anti-flow pattern, where players will cut the slider short in order to move to the next object. The most natural jump pattern is (x--x). - // Case (2) is a flow pattern, where players will follow the slider through to its visual extent. The most natural jump pattern is (>--x). + // <======o==>---o + // ↑ + // most natural jump path // - // A lenience is applied by assuming that the player jumps the minimum of these two distances in all cases. + // In this case the most natural movement path is better approximated by a new distance called "tailJumpDistance" - the distance between the slider's tail and the next hitobject. + // + // Thus, the player is assumed to jump the minimum of these two distances in all cases. // float tailJumpDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor; From 99991a6703887ffa1b95b4217005bf81db15be4d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 8 Dec 2021 15:59:15 +0900 Subject: [PATCH 247/419] Minor cleanups, unifying wording a bit more --- .../Difficulty/Preprocessing/OsuDifficultyHitObject.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 2c81b42e6c..4df8ff0b12 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, min_delta_time); // - // There are two types of slider-to-object patterns to consider in order to better approximate the real movements a player will take. + // There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects. // // 1. The anti-flow pattern, where players cut the slider short in order to move to the next hitobject. // @@ -127,7 +127,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing // | ← most natural jump path // o ← a follow-up hitcircle // - // In this case the most natural jump path (o--o) is approximated by LazyJumpDistance. + // In this case the most natural jump path is approximated by LazyJumpDistance. // // 2. The flow pattern, where players follow through the slider to its visual extent into the next hitobject. // @@ -135,7 +135,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing // ↑ // most natural jump path // - // In this case the most natural movement path is better approximated by a new distance called "tailJumpDistance" - the distance between the slider's tail and the next hitobject. + // In this case the most natural jump path is better approximated by a new distance called "tailJumpDistance" - the distance between the slider's tail and the next hitobject. // // Thus, the player is assumed to jump the minimum of these two distances in all cases. // From af1e97b7c73fb476c9208051d8f47fa9aa579837 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Wed, 8 Dec 2021 09:49:36 +0100 Subject: [PATCH 248/419] Move playing text added samples to private helper and fix it never playing the last sample `RNG.Next` is exclusive of the upper bound, meaning that the last sample would never be played. --- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 96319b9fdd..145e51b05a 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -86,6 +86,8 @@ namespace osu.Game.Graphics.UserInterface protected override Color4 SelectionColour => selectionColour; + private void playTextAddedSample() => textAddedSamples[RNG.Next(0, textAddedSamples.Length)]?.Play(); + protected override void OnUserTextAdded(string added) { base.OnUserTextAdded(added); @@ -93,7 +95,7 @@ namespace osu.Game.Graphics.UserInterface if (added.Any(char.IsUpper) && AllowUniqueCharacterSamples) capsTextAddedSample?.Play(); else - textAddedSamples[RNG.Next(0, 3)]?.Play(); + playTextAddedSample(); } protected override void OnUserTextRemoved(string removed) From de89e321c8e57d3b914bbbbdb05c774e5cc43ed1 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Wed, 8 Dec 2021 09:57:53 +0100 Subject: [PATCH 249/419] Add sounds for IME composition --- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 145e51b05a..709f64d16d 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -119,6 +119,70 @@ namespace osu.Game.Graphics.UserInterface caretMovedSample?.Play(); } + protected override void OnImeComposition(string newComposition, int removedTextLength, int addedTextLength, bool caretMoved) + { + base.OnImeComposition(newComposition, removedTextLength, addedTextLength, caretMoved); + + if (string.IsNullOrEmpty(newComposition)) + { + switch (removedTextLength) + { + case 0: + // empty composition event, composition wasn't changed, don't play anything. + return; + + case 1: + // composition probably ended by pressing backspace, or was cancelled. + textRemovedSample?.Play(); + return; + + default: + // longer text removed, composition ended because it was cancelled. + // could be a different sample if desired. + textRemovedSample?.Play(); + return; + } + } + + if (addedTextLength > 0) + { + // some text was added, probably due to typing new text or by changing the candidate. + playTextAddedSample(); + return; + } + + if (removedTextLength > 0) + { + // text was probably removed by backspacing. + // it's also possible that a candidate that only removed text was changed to. + textRemovedSample?.Play(); + return; + } + + if (caretMoved) + { + // only the caret/selection was moved. + caretMovedSample?.Play(); + } + } + + protected override void OnImeResult(string result, bool successful) + { + base.OnImeResult(result, successful); + + if (successful) + { + // composition was successfully completed, usually by pressing the enter key. + textCommittedSample?.Play(); + } + else + { + // composition was prematurely ended, eg. by clicking inside the textbox. + // could be a different sample if desired. + textCommittedSample?.Play(); + } + } + protected override void OnFocus(FocusEvent e) { BorderThickness = 3; From 8fa73fcbf695c21911c8c9afda25fe9ee49e1928 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Dec 2021 18:30:08 +0900 Subject: [PATCH 250/419] Move helper method to end of class --- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 709f64d16d..6db3068d84 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -86,8 +86,6 @@ namespace osu.Game.Graphics.UserInterface protected override Color4 SelectionColour => selectionColour; - private void playTextAddedSample() => textAddedSamples[RNG.Next(0, textAddedSamples.Length)]?.Play(); - protected override void OnUserTextAdded(string added) { base.OnUserTextAdded(added); @@ -208,6 +206,8 @@ namespace osu.Game.Graphics.UserInterface SelectionColour = SelectionColour, }; + private void playTextAddedSample() => textAddedSamples[RNG.Next(0, textAddedSamples.Length)]?.Play(); + private class OsuCaret : Caret { private const float caret_move_time = 60; From beb5d61a42cc69841cfdbb1c8de0cc36d5973535 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 8 Dec 2021 20:38:18 +0900 Subject: [PATCH 251/419] Separate playlist item deletion to Playlists-specific class --- .../TestSceneDrawableRoomPlaylist.cs | 97 +-------- .../TestScenePlaylistsRoomPlaylist.cs | 188 ++++++++++++++++++ .../Components/MatchBeatmapDetailArea.cs | 3 +- .../OnlinePlay/DrawableRoomPlaylist.cs | 40 +--- .../DrawableRoomPlaylistWithResults.cs | 2 +- .../Playlists/PlaylistsRoomPlaylist.cs | 24 +++ .../Playlists/PlaylistsRoomSettingsOverlay.cs | 5 +- 7 files changed, 227 insertions(+), 132 deletions(-) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomPlaylist.cs create mode 100644 osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPlaylist.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 55aa665ff1..c60b55d7e8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -128,95 +128,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("item 1 is selected", () => playlist.SelectedItem.Value == playlist.Items[1]); } - [Test] - public void TestItemRemovedOnDeletion() - { - PlaylistItem selectedItem = null; - - createPlaylist(true, true); - - moveToItem(0); - AddStep("click", () => InputManager.Click(MouseButton.Left)); - AddStep("retrieve selection", () => selectedItem = playlist.SelectedItem.Value); - - moveToDeleteButton(0); - AddStep("click delete button", () => InputManager.Click(MouseButton.Left)); - - AddAssert("item removed", () => !playlist.Items.Contains(selectedItem)); - } - - [Test] - public void TestNextItemSelectedAfterDeletion() - { - createPlaylist(true, true); - - moveToItem(0); - AddStep("click", () => InputManager.Click(MouseButton.Left)); - - moveToDeleteButton(0); - AddStep("click delete button", () => InputManager.Click(MouseButton.Left)); - - AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]); - } - - [Test] - public void TestLastItemSelectedAfterLastItemDeleted() - { - createPlaylist(true, true); - - AddWaitStep("wait for flow", 5); // Items may take 1 update frame to flow. A wait count of 5 is guaranteed to result in the flow being updated as desired. - AddStep("scroll to bottom", () => playlist.ChildrenOfType>().First().ScrollToEnd(false)); - - moveToItem(19); - AddStep("click", () => InputManager.Click(MouseButton.Left)); - - moveToDeleteButton(19); - AddStep("click delete button", () => InputManager.Click(MouseButton.Left)); - - AddAssert("item 18 is selected", () => playlist.SelectedItem.Value == playlist.Items[18]); - } - - [Test] - public void TestSelectionResetWhenAllItemsDeleted() - { - createPlaylist(true, true); - - AddStep("remove all but one item", () => - { - playlist.Items.RemoveRange(1, playlist.Items.Count - 1); - }); - - moveToItem(0); - AddStep("click", () => InputManager.Click(MouseButton.Left)); - moveToDeleteButton(0); - AddStep("click delete button", () => InputManager.Click(MouseButton.Left)); - - AddAssert("no item selected", () => playlist.SelectedItem.Value == null); - } - - // Todo: currently not possible due to bindable list shortcomings (https://github.com/ppy/osu-framework/issues/3081) - // [Test] - public void TestNextItemSelectedAfterExternalDeletion() - { - createPlaylist(true, true); - - moveToItem(0); - AddStep("click", () => InputManager.Click(MouseButton.Left)); - AddStep("remove item 0", () => playlist.Items.RemoveAt(0)); - - AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]); - } - - [Test] - public void TestChangeBeatmapAndRemove() - { - createPlaylist(true, true); - - AddStep("change beatmap of first item", () => playlist.Items[0].BeatmapID = 30); - moveToDeleteButton(0); - AddStep("click delete button", () => InputManager.Click(MouseButton.Left)); - } - [Test] public void TestDownloadButtonHiddenWhenBeatmapExists() { @@ -326,12 +237,6 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.MoveMouseTo(item.ChildrenOfType.PlaylistItemHandle>().Single(), offset); }); - private void moveToDeleteButton(int index, Vector2? offset = null) => AddStep($"move mouse to delete button {index}", () => - { - var item = playlist.ChildrenOfType>().ElementAt(index); - InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0), offset); - }); - private void assertHandleVisibility(int index, bool visible) => AddAssert($"handle {index} {(visible ? "is" : "is not")} visible", () => (playlist.ChildrenOfType.PlaylistItemHandle>().ElementAt(index).Alpha > 0) == visible); @@ -425,7 +330,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public new IReadOnlyDictionary> ItemMap => base.ItemMap; public TestPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false) - : base(allowEdit, allowSelection, showItemOwner: showItemOwner) + : base(allowEdit, allowSelection, showItemOwner) { } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomPlaylist.cs new file mode 100644 index 0000000000..5b0c7c7d55 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomPlaylist.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.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Database; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Playlists; +using osu.Game.Tests.Beatmaps; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestScenePlaylistsRoomPlaylist : OsuManualInputManagerTestScene + { + private TestPlaylist playlist; + + [Cached(typeof(UserLookupCache))] + private readonly TestUserLookupCache userLookupCache = new TestUserLookupCache(); + + [Test] + public void TestItemRemovedOnDeletion() + { + PlaylistItem selectedItem = null; + + createPlaylist(true, true); + + moveToItem(0); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddStep("retrieve selection", () => selectedItem = playlist.SelectedItem.Value); + + moveToDeleteButton(0); + AddStep("click delete button", () => InputManager.Click(MouseButton.Left)); + + AddAssert("item removed", () => !playlist.Items.Contains(selectedItem)); + } + + [Test] + public void TestNextItemSelectedAfterDeletion() + { + createPlaylist(true, true); + + moveToItem(0); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + + moveToDeleteButton(0); + AddStep("click delete button", () => InputManager.Click(MouseButton.Left)); + + AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]); + } + + [Test] + public void TestLastItemSelectedAfterLastItemDeleted() + { + createPlaylist(true, true); + + AddWaitStep("wait for flow", 5); // Items may take 1 update frame to flow. A wait count of 5 is guaranteed to result in the flow being updated as desired. + AddStep("scroll to bottom", () => playlist.ChildrenOfType>().First().ScrollToEnd(false)); + + moveToItem(19); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + + moveToDeleteButton(19); + AddStep("click delete button", () => InputManager.Click(MouseButton.Left)); + + AddAssert("item 18 is selected", () => playlist.SelectedItem.Value == playlist.Items[18]); + } + + [Test] + public void TestSelectionResetWhenAllItemsDeleted() + { + createPlaylist(true, true); + + AddStep("remove all but one item", () => + { + playlist.Items.RemoveRange(1, playlist.Items.Count - 1); + }); + + moveToItem(0); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + moveToDeleteButton(0); + AddStep("click delete button", () => InputManager.Click(MouseButton.Left)); + + AddAssert("no item selected", () => playlist.SelectedItem.Value == null); + } + + // Todo: currently not possible due to bindable list shortcomings (https://github.com/ppy/osu-framework/issues/3081) + // [Test] + public void TestNextItemSelectedAfterExternalDeletion() + { + createPlaylist(true, true); + + moveToItem(0); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddStep("remove item 0", () => playlist.Items.RemoveAt(0)); + + AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]); + } + + [Test] + public void TestChangeBeatmapAndRemove() + { + createPlaylist(true, true); + + AddStep("change beatmap of first item", () => playlist.Items[0].BeatmapID = 30); + moveToDeleteButton(0); + AddStep("click delete button", () => InputManager.Click(MouseButton.Left)); + } + + private void moveToItem(int index, Vector2? offset = null) + => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset)); + + private void moveToDeleteButton(int index, Vector2? offset = null) => AddStep($"move mouse to delete button {index}", () => + { + var item = playlist.ChildrenOfType>().ElementAt(index); + InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0), offset); + }); + + private void createPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false) + { + AddStep("create playlist", () => + { + Child = playlist = new TestPlaylist(allowEdit, allowSelection, showItemOwner) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(500, 300) + }; + + for (int i = 0; i < 20; i++) + { + playlist.Items.Add(new PlaylistItem + { + ID = i, + OwnerID = 2, + Beatmap = + { + Value = i % 2 == 1 + ? new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo + : new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Artist = "Artist", + Author = new APIUser { Username = "Creator name here" }, + Title = "Long title used to check background colour", + }, + BeatmapSet = new BeatmapSetInfo() + } + }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RequiredMods = + { + new OsuModHardRock(), + new OsuModDoubleTime(), + new OsuModAutoplay() + } + }); + } + }); + + AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded)); + } + + private class TestPlaylist : PlaylistsRoomPlaylist + { + public new IReadOnlyDictionary> ItemMap => base.ItemMap; + + public TestPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false) + : base(allowEdit, allowSelection, showItemOwner) + { + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs index b013cbafd8..7afebb04af 100644 --- a/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs +++ b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Select; using osuTK; @@ -43,7 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Bottom = 10 }, - Child = playlist = new DrawableRoomPlaylist(true, false) + Child = playlist = new PlaylistsRoomPlaylist(true, false) { RelativeSizeAxes = Axes.Both, } diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index f2d31c8e67..35d1fb33ad 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -1,9 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Specialized; +using System; using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; @@ -16,6 +15,11 @@ namespace osu.Game.Screens.OnlinePlay { public readonly Bindable SelectedItem = new Bindable(); + /// + /// Invoked when an item is requested to be deleted. + /// + public Action DeletionRequested; + private readonly bool allowEdit; private readonly bool allowSelection; private readonly bool showItemOwner; @@ -27,23 +31,6 @@ namespace osu.Game.Screens.OnlinePlay this.showItemOwner = showItemOwner; } - protected override void LoadComplete() - { - base.LoadComplete(); - - // Scheduled since items are removed and re-added upon rearrangement - Items.CollectionChanged += (_, args) => Schedule(() => - { - switch (args.Action) - { - case NotifyCollectionChangedAction.Remove: - if (allowSelection && args.OldItems.Contains(SelectedItem)) - SelectedItem.Value = null; - break; - } - }); - } - protected override ScrollContainer CreateScrollContainer() => base.CreateScrollContainer().With(d => { d.ScrollbarVisible = false; @@ -57,20 +44,7 @@ namespace osu.Game.Screens.OnlinePlay protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) => new DrawableRoomPlaylistItem(item, allowEdit, allowSelection, showItemOwner) { SelectedItem = { BindTarget = SelectedItem }, - RequestDeletion = requestDeletion + RequestDeletion = i => DeletionRequested?.Invoke(i) }; - - private void requestDeletion(PlaylistItem item) - { - if (allowSelection && SelectedItem.Value == item) - { - if (Items.Count == 1) - SelectedItem.Value = null; - else - SelectedItem.Value = Items.GetNext(item) ?? Items[^2]; - } - - Items.Remove(item); - } } } diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs index 8b1bb7abc1..1acd239fc8 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.OnlinePlay private readonly bool showItemOwner; public DrawableRoomPlaylistWithResults(bool showItemOwner = false) - : base(false, true, showItemOwner: showItemOwner) + : base(false, true, showItemOwner) { this.showItemOwner = showItemOwner; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPlaylist.cs new file mode 100644 index 0000000000..de0960940d --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPlaylist.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + public class PlaylistsRoomPlaylist : DrawableRoomPlaylist + { + public PlaylistsRoomPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false) + : base(allowEdit, allowSelection, showItemOwner) + { + DeletionRequested = item => + { + var nextItem = Items.GetNext(item); + + Items.Remove(item); + + SelectedItem.Value = nextItem ?? Items.LastOrDefault(); + }; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index 27c8dc1120..b903e9cb7b 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -205,7 +205,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { new Drawable[] { - playlist = new DrawableRoomPlaylist(true, false) { RelativeSizeAxes = Axes.Both } + playlist = new PlaylistsRoomPlaylist(true, false) + { + RelativeSizeAxes = Axes.Both + } }, new Drawable[] { From 3be4d8b68de9ac9c5286917dd85be79470849920 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Dec 2021 01:04:28 +0900 Subject: [PATCH 252/419] Remove ctor params from DrawableRoomPlaylist/DrawablePlaylistItem --- .../TestSceneDrawableRoomPlaylist.cs | 18 ++++- .../TestScenePlaylistsRoomPlaylist.cs | 20 +++--- .../Components/MatchBeatmapDetailArea.cs | 2 +- .../OnlinePlay/DrawableRoomPlaylist.cs | 14 +--- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 66 ++++++++++++------- .../Match/MultiplayerMatchSettingsOverlay.cs | 2 +- .../Match/Playlist/MultiplayerHistoryList.cs | 9 ++- .../Match/Playlist/MultiplayerQueueList.cs | 9 ++- .../Playlists/PlaylistsRoomPlaylist.cs | 23 ++++++- .../Playlists/PlaylistsRoomSettingsOverlay.cs | 2 +- 10 files changed, 105 insertions(+), 60 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index c60b55d7e8..449fdaf0aa 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -329,10 +329,26 @@ namespace osu.Game.Tests.Visual.Multiplayer { public new IReadOnlyDictionary> ItemMap => base.ItemMap; + private readonly bool allowEdit; + private readonly bool allowSelection; + private readonly bool showItemOwner; + public TestPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false) - : base(allowEdit, allowSelection, showItemOwner) { + this.allowEdit = allowEdit; + this.allowSelection = allowSelection; + this.showItemOwner = showItemOwner; } + + protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) => base.CreateOsuDrawable(item).With(d => + { + var drawablePlaylistItem = (DrawableRoomPlaylistItem)d; + + drawablePlaylistItem.AllowReordering = allowEdit; + drawablePlaylistItem.AllowDeletion = allowEdit; + drawablePlaylistItem.AllowSelection = allowSelection; + drawablePlaylistItem.ShowItemOwner = showItemOwner; + }); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomPlaylist.cs index 5b0c7c7d55..8312964b50 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomPlaylist.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { PlaylistItem selectedItem = null; - createPlaylist(true, true); + createPlaylist(); moveToItem(0); AddStep("click", () => InputManager.Click(MouseButton.Left)); @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestNextItemSelectedAfterDeletion() { - createPlaylist(true, true); + createPlaylist(); moveToItem(0); AddStep("click", () => InputManager.Click(MouseButton.Left)); @@ -65,7 +65,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestLastItemSelectedAfterLastItemDeleted() { - createPlaylist(true, true); + createPlaylist(); AddWaitStep("wait for flow", 5); // Items may take 1 update frame to flow. A wait count of 5 is guaranteed to result in the flow being updated as desired. AddStep("scroll to bottom", () => playlist.ChildrenOfType>().First().ScrollToEnd(false)); @@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestSelectionResetWhenAllItemsDeleted() { - createPlaylist(true, true); + createPlaylist(); AddStep("remove all but one item", () => { @@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Multiplayer // [Test] public void TestNextItemSelectedAfterExternalDeletion() { - createPlaylist(true, true); + createPlaylist(); moveToItem(0); AddStep("click", () => InputManager.Click(MouseButton.Left)); @@ -113,7 +113,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestChangeBeatmapAndRemove() { - createPlaylist(true, true); + createPlaylist(); AddStep("change beatmap of first item", () => playlist.Items[0].BeatmapID = 30); moveToDeleteButton(0); @@ -129,11 +129,11 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0), offset); }); - private void createPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false) + private void createPlaylist() { AddStep("create playlist", () => { - Child = playlist = new TestPlaylist(allowEdit, allowSelection, showItemOwner) + Child = playlist = new TestPlaylist { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -179,8 +179,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { public new IReadOnlyDictionary> ItemMap => base.ItemMap; - public TestPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false) - : base(allowEdit, allowSelection, showItemOwner) + public TestPlaylist() + : base(true, true, true) { } } diff --git a/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs index 7afebb04af..761f4818e0 100644 --- a/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs +++ b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Bottom = 10 }, - Child = playlist = new PlaylistsRoomPlaylist(true, false) + Child = playlist = new PlaylistsRoomPlaylist(true, true, true) { RelativeSizeAxes = Axes.Both, } diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 35d1fb33ad..2eb8cd8997 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -20,16 +20,6 @@ namespace osu.Game.Screens.OnlinePlay /// public Action DeletionRequested; - private readonly bool allowEdit; - private readonly bool allowSelection; - private readonly bool showItemOwner; - - public DrawableRoomPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false) - { - this.allowEdit = allowEdit; - this.allowSelection = allowSelection; - this.showItemOwner = showItemOwner; - } protected override ScrollContainer CreateScrollContainer() => base.CreateScrollContainer().With(d => { @@ -41,10 +31,10 @@ namespace osu.Game.Screens.OnlinePlay Spacing = new Vector2(0, 2) }; - protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) => new DrawableRoomPlaylistItem(item, allowEdit, allowSelection, showItemOwner) + protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) => new DrawableRoomPlaylistItem(item) { SelectedItem = { BindTarget = SelectedItem }, - RequestDeletion = i => DeletionRequested?.Invoke(i) + RequestDeletion = i => DeletionRequested?.Invoke(i), }; } } diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index c291bddeeb..1814e172df 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -53,6 +52,7 @@ namespace osu.Game.Screens.OnlinePlay private ModDisplay modDisplay; private FillFlowContainer buttonsFlow; private UpdateableAvatar ownerAvatar; + private Drawable removeButton; private readonly IBindable valid = new Bindable(); @@ -75,31 +75,20 @@ namespace osu.Game.Screens.OnlinePlay private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both }; - private readonly bool allowEdit; - private readonly bool allowSelection; - private readonly bool showItemOwner; - private FillFlowContainer mainFillFlow; - protected override bool ShouldBeConsideredForInput(Drawable child) => allowEdit || !allowSelection || SelectedItem.Value == Model; + protected override bool ShouldBeConsideredForInput(Drawable child) => AllowReordering || AllowDeletion || !AllowSelection || SelectedItem.Value == Model; - public DrawableRoomPlaylistItem(PlaylistItem item, bool allowEdit, bool allowSelection, bool showItemOwner) + public DrawableRoomPlaylistItem(PlaylistItem item) : base(item) { Item = item; - // TODO: edit support should be moved out into a derived class - this.allowEdit = allowEdit; - this.allowSelection = allowSelection; - this.showItemOwner = showItemOwner; - beatmap.BindTo(item.Beatmap); valid.BindTo(item.Valid); ruleset.BindTo(item.Ruleset); requiredMods.BindTo(item.RequiredMods); - ShowDragHandle.Value = allowEdit; - if (item.Expired) Colour = OsuColour.Gray(0.5f); } @@ -107,9 +96,6 @@ namespace osu.Game.Screens.OnlinePlay [BackgroundDependencyLoader] private void load() { - if (!allowEdit) - HandleColour = HandleColour.Opacity(0); - maskingContainer.BorderColour = colours.Yellow; } @@ -169,6 +155,42 @@ namespace osu.Game.Screens.OnlinePlay refresh(); } + public bool AllowSelection { get; set; } + + public bool AllowReordering + { + get => ShowDragHandle.Value; + set => ShowDragHandle.Value = value; + } + + private bool allowDeletion; + + public bool AllowDeletion + { + get => allowDeletion; + set + { + allowDeletion = value; + + if (removeButton != null) + removeButton.Alpha = value ? 1 : 0; + } + } + + private bool showItemOwner; + + public bool ShowItemOwner + { + get => showItemOwner; + set + { + showItemOwner = value; + + if (ownerAvatar != null) + ownerAvatar.Alpha = value ? 1 : 0; + } + } + private void refresh() { if (!valid.Value) @@ -336,7 +358,7 @@ namespace osu.Game.Screens.OnlinePlay Margin = new MarginPadding { Right = 8 }, Masking = true, CornerRadius = 4, - Alpha = showItemOwner ? 1 : 0 + Alpha = ShowItemOwner ? 1 : 0 }, } } @@ -349,11 +371,11 @@ namespace osu.Game.Screens.OnlinePlay new[] { Item.Beatmap.Value == null ? Empty() : new PlaylistDownloadButton(Item), - new PlaylistRemoveButton + removeButton = new PlaylistRemoveButton { Size = new Vector2(30, 30), - Alpha = allowEdit ? 1 : 0, - Action = () => RequestDeletion?.Invoke(Model), + Alpha = AllowDeletion ? 1 : 0, + Action = () => RequestDeletion?.Invoke(Item), }, }; @@ -374,7 +396,7 @@ namespace osu.Game.Screens.OnlinePlay protected override bool OnClick(ClickEvent e) { - if (allowSelection && valid.Value) + if (AllowSelection && valid.Value) SelectedItem.Value = Model; return true; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 39d60a0b05..7f1db733b3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -248,7 +248,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Spacing = new Vector2(5), Children = new Drawable[] { - drawablePlaylist = new DrawableRoomPlaylist(false, false) + drawablePlaylist = new DrawableRoomPlaylist { RelativeSizeAxes = Axes.X, Height = DrawableRoomPlaylistItem.HEIGHT diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs index d708b39898..7102738271 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; using osu.Game.Online.Rooms; using osuTK; @@ -15,16 +16,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist ///
public class MultiplayerHistoryList : DrawableRoomPlaylist { - public MultiplayerHistoryList() - : base(false, false, true) - { - } - protected override FillFlowContainer> CreateListFillFlowContainer() => new HistoryFillFlowContainer { Spacing = new Vector2(0, 2) }; + protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) + => base.CreateOsuDrawable(item).With(d => ((DrawableRoomPlaylistItem)d).ShowItemOwner = true); + private class HistoryFillFlowContainer : FillFlowContainer> { public override IEnumerable FlowingChildren => base.FlowingChildren.OfType>().OrderByDescending(item => item.Model.PlayedAt); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index 1b1b66273f..814ea48646 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; using osu.Game.Online.Rooms; using osuTK; @@ -17,16 +18,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist ///
public class MultiplayerQueueList : DrawableRoomPlaylist { - public MultiplayerQueueList() - : base(false, false, true) - { - } - protected override FillFlowContainer> CreateListFillFlowContainer() => new QueueFillFlowContainer { Spacing = new Vector2(0, 2) }; + protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) + => base.CreateOsuDrawable(item).With(d => ((DrawableRoomPlaylistItem)d).ShowItemOwner = true); + private class QueueFillFlowContainer : FillFlowContainer> { [Resolved(typeof(Room), nameof(Room.Playlist))] diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPlaylist.cs index de0960940d..f4df9c4406 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPlaylist.cs @@ -3,14 +3,24 @@ using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Playlists { public class PlaylistsRoomPlaylist : DrawableRoomPlaylist { - public PlaylistsRoomPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false) - : base(allowEdit, allowSelection, showItemOwner) + private readonly bool allowReordering; + private readonly bool allowDeletion; + private readonly bool allowSelection; + + public PlaylistsRoomPlaylist(bool allowReordering, bool allowDeletion, bool allowSelection) { + this.allowReordering = allowReordering; + this.allowDeletion = allowDeletion; + this.allowSelection = allowSelection; + DeletionRequested = item => { var nextItem = Items.GetNext(item); @@ -20,5 +30,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists SelectedItem.Value = nextItem ?? Items.LastOrDefault(); }; } + + protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) => base.CreateOsuDrawable(item).With(d => + { + var drawablePlaylistItem = (DrawableRoomPlaylistItem)d; + + drawablePlaylistItem.AllowReordering = allowReordering; + drawablePlaylistItem.AllowDeletion = allowDeletion; + drawablePlaylistItem.AllowSelection = allowSelection; + }); } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index b903e9cb7b..40b0bc7571 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -205,7 +205,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { new Drawable[] { - playlist = new PlaylistsRoomPlaylist(true, false) + playlist = new PlaylistsRoomPlaylist(true, true, false) { RelativeSizeAxes = Axes.Both } From 26f6c5e5a5571469a810b54a68049da8a74461e2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Dec 2021 01:16:37 +0900 Subject: [PATCH 253/419] Remove ctor params from PlaylistsRoomPlaylist --- .../TestSceneDrawableRoomPlaylist.cs | 44 ++++---------- .../TestScenePlaylistsRoomPlaylist.cs | 2 +- .../Components/MatchBeatmapDetailArea.cs | 3 +- .../OnlinePlay/DrawableRoomPlaylist.cs | 60 +++++++++++++++++++ .../Playlists/PlaylistsRoomPlaylist.cs | 23 +------ .../Playlists/PlaylistsRoomSettingsOverlay.cs | 4 +- 6 files changed, 80 insertions(+), 56 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 449fdaf0aa..269cd15a0f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -48,7 +49,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestNonEditableNonSelectable() { - createPlaylist(false, false); + createPlaylist(); moveToItem(0); assertHandleVisibility(0, false); @@ -61,7 +62,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestEditable() { - createPlaylist(true, false); + createPlaylist(p => p.AllowReordering = p.AllowDeletion = true); moveToItem(0); assertHandleVisibility(0, true); @@ -74,7 +75,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestMarkInvalid() { - createPlaylist(true, true); + createPlaylist(p => p.AllowReordering = p.AllowDeletion = p.AllowSelection = true); AddStep("mark item 0 as invalid", () => playlist.Items[0].MarkInvalid()); @@ -87,7 +88,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestSelectable() { - createPlaylist(false, true); + createPlaylist(p => p.AllowSelection = true); moveToItem(0); assertHandleVisibility(0, false); @@ -101,7 +102,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestEditableSelectable() { - createPlaylist(true, true); + createPlaylist(p => p.AllowReordering = p.AllowDeletion = p.AllowSelection = true); moveToItem(0); assertHandleVisibility(0, true); @@ -115,7 +116,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestSelectionNotLostAfterRearrangement() { - createPlaylist(true, true); + createPlaylist(p => p.AllowReordering = p.AllowDeletion = p.AllowSelection = true); moveToItem(0); AddStep("click", () => InputManager.Click(MouseButton.Left)); @@ -180,7 +181,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create playlist", () => { - Child = playlist = new TestPlaylist(false, false) + Child = playlist = new TestPlaylist { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -223,7 +224,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [TestCase(true)] public void TestWithOwner(bool withOwner) { - createPlaylist(false, false, withOwner); + createPlaylist(p => p.ShowItemOwners = withOwner); AddAssert("owner visible", () => playlist.ChildrenOfType().All(a => a.IsPresent == withOwner)); } @@ -245,11 +246,11 @@ namespace osu.Game.Tests.Visual.Multiplayer => AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible", () => (playlist.ChildrenOfType().ElementAt(2 + index * 2).Alpha > 0) == visible); - private void createPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false) + private void createPlaylist(Action setupPlaylist) { AddStep("create playlist", () => { - Child = playlist = new TestPlaylist(allowEdit, allowSelection, showItemOwner) + Child = playlist = new TestPlaylist { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -295,7 +296,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create playlist", () => { - Child = playlist = new TestPlaylist(false, false) + Child = playlist = new TestPlaylist { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -328,27 +329,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private class TestPlaylist : DrawableRoomPlaylist { public new IReadOnlyDictionary> ItemMap => base.ItemMap; - - private readonly bool allowEdit; - private readonly bool allowSelection; - private readonly bool showItemOwner; - - public TestPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false) - { - this.allowEdit = allowEdit; - this.allowSelection = allowSelection; - this.showItemOwner = showItemOwner; - } - - protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) => base.CreateOsuDrawable(item).With(d => - { - var drawablePlaylistItem = (DrawableRoomPlaylistItem)d; - - drawablePlaylistItem.AllowReordering = allowEdit; - drawablePlaylistItem.AllowDeletion = allowEdit; - drawablePlaylistItem.AllowSelection = allowSelection; - drawablePlaylistItem.ShowItemOwner = showItemOwner; - }); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomPlaylist.cs index 8312964b50..264f6aa2c5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomPlaylist.cs @@ -180,8 +180,8 @@ namespace osu.Game.Tests.Visual.Multiplayer public new IReadOnlyDictionary> ItemMap => base.ItemMap; public TestPlaylist() - : base(true, true, true) { + AllowSelection = true; } } } diff --git a/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs index 761f4818e0..d56acff8c7 100644 --- a/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs +++ b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs @@ -44,9 +44,10 @@ namespace osu.Game.Screens.OnlinePlay.Components { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Bottom = 10 }, - Child = playlist = new PlaylistsRoomPlaylist(true, true, true) + Child = playlist = new PlaylistsRoomPlaylist { RelativeSizeAxes = Axes.Both, + AllowSelection = true, } } }, diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 2eb8cd8997..4389f40afc 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -20,6 +21,61 @@ namespace osu.Game.Screens.OnlinePlay ///
public Action DeletionRequested; + private bool allowReordering; + + public bool AllowReordering + { + get => allowReordering; + set + { + allowReordering = value; + + foreach (var item in ListContainer.OfType()) + item.AllowReordering = value; + } + } + + private bool allowDeletion; + + public bool AllowDeletion + { + get => allowDeletion; + set + { + allowDeletion = value; + + foreach (var item in ListContainer.OfType()) + item.AllowDeletion = value; + } + } + + private bool allowSelection; + + public bool AllowSelection + { + get => allowSelection; + set + { + allowSelection = value; + + foreach (var item in ListContainer.OfType()) + item.AllowSelection = value; + } + } + + private bool showItemOwners; + + public bool ShowItemOwners + { + get => showItemOwners; + set + { + showItemOwners = value; + + foreach (var item in ListContainer.OfType()) + item.ShowItemOwner = value; + } + } protected override ScrollContainer CreateScrollContainer() => base.CreateScrollContainer().With(d => { @@ -35,6 +91,10 @@ namespace osu.Game.Screens.OnlinePlay { SelectedItem = { BindTarget = SelectedItem }, RequestDeletion = i => DeletionRequested?.Invoke(i), + AllowReordering = AllowReordering, + AllowDeletion = AllowDeletion, + AllowSelection = AllowSelection, + ShowItemOwner = ShowItemOwners, }; } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPlaylist.cs index f4df9c4406..d8c316b642 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPlaylist.cs @@ -3,23 +3,15 @@ using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Playlists { public class PlaylistsRoomPlaylist : DrawableRoomPlaylist { - private readonly bool allowReordering; - private readonly bool allowDeletion; - private readonly bool allowSelection; - - public PlaylistsRoomPlaylist(bool allowReordering, bool allowDeletion, bool allowSelection) + public PlaylistsRoomPlaylist() { - this.allowReordering = allowReordering; - this.allowDeletion = allowDeletion; - this.allowSelection = allowSelection; + AllowReordering = true; + AllowDeletion = true; DeletionRequested = item => { @@ -30,14 +22,5 @@ namespace osu.Game.Screens.OnlinePlay.Playlists SelectedItem.Value = nextItem ?? Items.LastOrDefault(); }; } - - protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) => base.CreateOsuDrawable(item).With(d => - { - var drawablePlaylistItem = (DrawableRoomPlaylistItem)d; - - drawablePlaylistItem.AllowReordering = allowReordering; - drawablePlaylistItem.AllowDeletion = allowDeletion; - drawablePlaylistItem.AllowSelection = allowSelection; - }); } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index 40b0bc7571..915ec356d5 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -205,9 +205,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { new Drawable[] { - playlist = new PlaylistsRoomPlaylist(true, true, false) + playlist = new PlaylistsRoomPlaylist { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, } }, new Drawable[] From be2dbf42c3a0f69b1487522676a8a0ba29c3e987 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Dec 2021 01:17:25 +0900 Subject: [PATCH 254/419] Flatten DrawableRoomPlaylistWithResults into base class --- .../OnlinePlay/DrawableRoomPlaylist.cs | 21 ++++++ .../OnlinePlay/DrawableRoomPlaylistItem.cs | 44 +++++++++++- .../DrawableRoomPlaylistWithResults.cs | 69 ------------------- .../Playlists/PlaylistsRoomSubScreen.cs | 6 +- 4 files changed, 66 insertions(+), 74 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 4389f40afc..e76d905849 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -21,6 +21,11 @@ namespace osu.Game.Screens.OnlinePlay ///
public Action DeletionRequested; + /// + /// Invoked to request showing the results for an item. + /// + public Action ShowResultsRequested; + private bool allowReordering; public bool AllowReordering @@ -63,6 +68,20 @@ namespace osu.Game.Screens.OnlinePlay } } + private bool allowShowingResults; + + public bool AllowShowingResults + { + get => allowShowingResults; + set + { + allowShowingResults = value; + + foreach (var item in ListContainer.OfType()) + item.AllowShowingResults = value; + } + } + private bool showItemOwners; public bool ShowItemOwners @@ -94,7 +113,9 @@ namespace osu.Game.Screens.OnlinePlay AllowReordering = AllowReordering, AllowDeletion = AllowDeletion, AllowSelection = AllowSelection, + AllowShowingResults = AllowShowingResults, ShowItemOwner = ShowItemOwners, + ShowResultsRequested = i => ShowResultsRequested?.Invoke(i) }; } } diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 1814e172df..28b4997b63 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -41,6 +41,7 @@ namespace osu.Game.Screens.OnlinePlay public const float ICON_HEIGHT = 34; public Action RequestDeletion; + public Action ShowResultsRequested; public readonly Bindable SelectedItem = new Bindable(); @@ -53,6 +54,7 @@ namespace osu.Game.Screens.OnlinePlay private FillFlowContainer buttonsFlow; private UpdateableAvatar ownerAvatar; private Drawable removeButton; + private Drawable showResultsButton; private readonly IBindable valid = new Bindable(); @@ -177,6 +179,20 @@ namespace osu.Game.Screens.OnlinePlay } } + private bool allowShowingResults; + + public bool AllowShowingResults + { + get => allowShowingResults; + set + { + allowShowingResults = value; + + if (showResultsButton != null) + showResultsButton.Alpha = value ? 1 : 0; + } + } + private bool showItemOwner; public bool ShowItemOwner @@ -230,7 +246,7 @@ namespace osu.Game.Screens.OnlinePlay modDisplay.Current.Value = requiredMods.ToArray(); buttonsFlow.Clear(); - buttonsFlow.ChildrenEnumerable = CreateButtons(); + buttonsFlow.ChildrenEnumerable = createButtons(); difficultyIconContainer.FadeInFromZero(500, Easing.OutQuint); mainFillFlow.FadeInFromZero(500, Easing.OutQuint); @@ -344,7 +360,7 @@ namespace osu.Game.Screens.OnlinePlay Margin = new MarginPadding { Horizontal = 8 }, AutoSizeAxes = Axes.Both, Spacing = new Vector2(5), - ChildrenEnumerable = CreateButtons().Select(button => button.With(b => + ChildrenEnumerable = createButtons().Select(button => button.With(b => { b.Anchor = Anchor.Centre; b.Origin = Anchor.Centre; @@ -367,9 +383,14 @@ namespace osu.Game.Screens.OnlinePlay }; } - protected virtual IEnumerable CreateButtons() => + private IEnumerable createButtons() => new[] { + showResultsButton = new ShowResultsButton + { + Action = () => ShowResultsRequested?.Invoke(Item), + Alpha = AllowShowingResults ? 1 : 0, + }, Item.Beatmap.Value == null ? Empty() : new PlaylistDownloadButton(Item), removeButton = new PlaylistRemoveButton { @@ -454,6 +475,23 @@ namespace osu.Game.Screens.OnlinePlay } } + private class ShowResultsButton : IconButton + { + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Icon = FontAwesome.Solid.ChartPie; + TooltipText = "View results"; + + Add(new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue, + Colour = colours.Gray4, + }); + } + } + // For now, this is the same implementation as in PanelBackground, but supports a beatmap info rather than a working beatmap private class PanelBackground : Container // todo: should be a buffered container (https://github.com/ppy/osu-framework/issues/3222) { diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs deleted file mode 100644 index 1acd239fc8..0000000000 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs +++ /dev/null @@ -1,69 +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.Linq; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Rooms; - -namespace osu.Game.Screens.OnlinePlay -{ - public class DrawableRoomPlaylistWithResults : DrawableRoomPlaylist - { - public Action RequestShowResults; - - private readonly bool showItemOwner; - - public DrawableRoomPlaylistWithResults(bool showItemOwner = false) - : base(false, true, showItemOwner) - { - this.showItemOwner = showItemOwner; - } - - protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) => - new DrawableRoomPlaylistItemWithResults(item, false, true, showItemOwner) - { - RequestShowResults = () => RequestShowResults(item), - SelectedItem = { BindTarget = SelectedItem }, - }; - - private class DrawableRoomPlaylistItemWithResults : DrawableRoomPlaylistItem - { - public Action RequestShowResults; - - public DrawableRoomPlaylistItemWithResults(PlaylistItem item, bool allowEdit, bool allowSelection, bool showItemOwner) - : base(item, allowEdit, allowSelection, showItemOwner) - { - } - - protected override IEnumerable CreateButtons() => - base.CreateButtons().Prepend(new FilledIconButton - { - Icon = FontAwesome.Solid.ChartPie, - Action = () => RequestShowResults?.Invoke(), - TooltipText = "View results" - }); - - private class FilledIconButton : IconButton - { - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Add(new Box - { - RelativeSizeAxes = Axes.Both, - Depth = float.MaxValue, - Colour = colours.Gray4, - }); - } - } - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 7e045802f7..dd2d22b742 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -88,12 +88,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists new Drawable[] { new OverlinedPlaylistHeader(), }, new Drawable[] { - new DrawableRoomPlaylistWithResults + new DrawableRoomPlaylist { RelativeSizeAxes = Axes.Both, Items = { BindTarget = Room.Playlist }, SelectedItem = { BindTarget = SelectedItem }, - RequestShowResults = item => + AllowSelection = true, + AllowShowingResults = true, + ShowResultsRequested = item => { Debug.Assert(RoomId.Value != null); ParentScreen?.Push(new PlaylistsResultsScreen(null, RoomId.Value.Value, item, false)); From 3b4833ca8eba289957601cab070d86703a74d1ae Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Dec 2021 01:29:45 +0900 Subject: [PATCH 255/419] A bit of cleanup + xmldocs on classes/members --- ...TestScenePlaylistsRoomSettingsPlaylist.cs} | 4 +- .../Components/MatchBeatmapDetailArea.cs | 2 +- .../OnlinePlay/DrawableRoomPlaylist.cs | 27 +++++++++- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 50 +++++++++++++------ .../Playlists/PlaylistsRoomSettingsOverlay.cs | 2 +- ...st.cs => PlaylistsRoomSettingsPlaylist.cs} | 7 ++- 6 files changed, 71 insertions(+), 21 deletions(-) rename osu.Game.Tests/Visual/Multiplayer/{TestScenePlaylistsRoomPlaylist.cs => TestScenePlaylistsRoomSettingsPlaylist.cs} (97%) rename osu.Game/Screens/OnlinePlay/Playlists/{PlaylistsRoomPlaylist.cs => PlaylistsRoomSettingsPlaylist.cs} (69%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs similarity index 97% rename from osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomPlaylist.cs rename to osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs index 264f6aa2c5..93ccd5f1e1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs @@ -24,7 +24,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestScenePlaylistsRoomPlaylist : OsuManualInputManagerTestScene + public class TestScenePlaylistsRoomSettingsPlaylist : OsuManualInputManagerTestScene { private TestPlaylist playlist; @@ -175,7 +175,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded)); } - private class TestPlaylist : PlaylistsRoomPlaylist + private class TestPlaylist : PlaylistsRoomSettingsPlaylist { public new IReadOnlyDictionary> ItemMap => base.ItemMap; diff --git a/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs index d56acff8c7..c29b8db26c 100644 --- a/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs +++ b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Bottom = 10 }, - Child = playlist = new PlaylistsRoomPlaylist + Child = playlist = new PlaylistsRoomSettingsPlaylist { RelativeSizeAxes = Axes.Both, AllowSelection = true, diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index e76d905849..5e58514705 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -12,8 +12,15 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay { + /// + /// A list scrollable list which displays the s in a . + /// public class DrawableRoomPlaylist : OsuRearrangeableListContainer { + /// + /// The currently-selected item, used to show a border around items. + /// May be updated by playlist items if is true. + /// public readonly Bindable SelectedItem = new Bindable(); /// @@ -22,12 +29,15 @@ namespace osu.Game.Screens.OnlinePlay public Action DeletionRequested; /// - /// Invoked to request showing the results for an item. + /// Invoked when an item requests its results to be shown. /// public Action ShowResultsRequested; private bool allowReordering; + /// + /// Whether to allow reordering items in the playlist. + /// public bool AllowReordering { get => allowReordering; @@ -42,6 +52,10 @@ namespace osu.Game.Screens.OnlinePlay private bool allowDeletion; + /// + /// Whether to allow deleting items from the playlist. + /// If true, requests to delete items may be satisfied via . + /// public bool AllowDeletion { get => allowDeletion; @@ -56,6 +70,10 @@ namespace osu.Game.Screens.OnlinePlay private bool allowSelection; + /// + /// Whether to allow selecting items from the playlist. + /// If true, clicking on items in the playlist will change the value of . + /// public bool AllowSelection { get => allowSelection; @@ -70,6 +88,10 @@ namespace osu.Game.Screens.OnlinePlay private bool allowShowingResults; + /// + /// Whether to allow items to request their results to be shown. + /// If true, requests to show the results may be satisfied via . + /// public bool AllowShowingResults { get => allowShowingResults; @@ -84,6 +106,9 @@ namespace osu.Game.Screens.OnlinePlay private bool showItemOwners; + /// + /// Whether to show the avatar of users which own each playlist item. + /// public bool ShowItemOwners { get => showItemOwners; diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 28b4997b63..fca944e9b6 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -40,11 +40,30 @@ namespace osu.Game.Screens.OnlinePlay public const float HEIGHT = 50; public const float ICON_HEIGHT = 34; + /// + /// Invoked when this item requests to be deleted. + /// public Action RequestDeletion; + + /// + /// Invoked when this item requests its results to be shown. + /// public Action ShowResultsRequested; + /// + /// The currently-selected item, used to show a border around this item. + /// May be updated by this item if is true. + /// public readonly Bindable SelectedItem = new Bindable(); + public readonly PlaylistItem Item; + + private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both }; + private readonly IBindable valid = new Bindable(); + private readonly Bindable beatmap = new Bindable(); + private readonly Bindable ruleset = new Bindable(); + private readonly BindableList requiredMods = new BindableList(); + private Container maskingContainer; private Container difficultyIconContainer; private LinkFlowContainer beatmapText; @@ -55,14 +74,8 @@ namespace osu.Game.Screens.OnlinePlay private UpdateableAvatar ownerAvatar; private Drawable removeButton; private Drawable showResultsButton; - - private readonly IBindable valid = new Bindable(); - - private readonly Bindable beatmap = new Bindable(); - private readonly Bindable ruleset = new Bindable(); - private readonly BindableList requiredMods = new BindableList(); - - public readonly PlaylistItem Item; + private PanelBackground panelBackground; + private FillFlowContainer mainFillFlow; [Resolved] private OsuColour colours { get; set; } @@ -73,12 +86,6 @@ namespace osu.Game.Screens.OnlinePlay [Resolved] private BeatmapLookupCache beatmapLookupCache { get; set; } - private PanelBackground panelBackground; - - private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both }; - - private FillFlowContainer mainFillFlow; - protected override bool ShouldBeConsideredForInput(Drawable child) => AllowReordering || AllowDeletion || !AllowSelection || SelectedItem.Value == Model; public DrawableRoomPlaylistItem(PlaylistItem item) @@ -157,8 +164,14 @@ namespace osu.Game.Screens.OnlinePlay refresh(); } + /// + /// Whether this item can be selected. + /// public bool AllowSelection { get; set; } + /// + /// Whether this item can be reordered in the playlist. + /// public bool AllowReordering { get => ShowDragHandle.Value; @@ -167,6 +180,9 @@ namespace osu.Game.Screens.OnlinePlay private bool allowDeletion; + /// + /// Whether this item can be deleted. + /// public bool AllowDeletion { get => allowDeletion; @@ -181,6 +197,9 @@ namespace osu.Game.Screens.OnlinePlay private bool allowShowingResults; + /// + /// Whether this item can have results shown. + /// public bool AllowShowingResults { get => allowShowingResults; @@ -195,6 +214,9 @@ namespace osu.Game.Screens.OnlinePlay private bool showItemOwner; + /// + /// Whether to display the avatar of the user which owns this playlist item. + /// public bool ShowItemOwner { get => showItemOwner; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index 915ec356d5..8f31422add 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -205,7 +205,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { new Drawable[] { - playlist = new PlaylistsRoomPlaylist + playlist = new PlaylistsRoomSettingsPlaylist { RelativeSizeAxes = Axes.Both, } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsPlaylist.cs similarity index 69% rename from osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPlaylist.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsPlaylist.cs index d8c316b642..bbe67e76e3 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsPlaylist.cs @@ -6,9 +6,12 @@ using osu.Framework.Extensions.IEnumerableExtensions; namespace osu.Game.Screens.OnlinePlay.Playlists { - public class PlaylistsRoomPlaylist : DrawableRoomPlaylist + /// + /// A which is displayed during the setup stage of a playlists room. + /// + public class PlaylistsRoomSettingsPlaylist : DrawableRoomPlaylist { - public PlaylistsRoomPlaylist() + public PlaylistsRoomSettingsPlaylist() { AllowReordering = true; AllowDeletion = true; From 273042aa16882993f553e9eb43fa44d2bdf25faa Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Dec 2021 01:47:46 +0900 Subject: [PATCH 256/419] Add virtual method for creating different DrawablePlaylistItem types --- .../OnlinePlay/DrawableRoomPlaylist.cs | 22 ++++++++++--------- .../Match/Playlist/MultiplayerHistoryList.cs | 9 ++++---- .../Match/Playlist/MultiplayerQueueList.cs | 9 ++++---- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 5e58514705..1abc7c47d6 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -131,16 +131,18 @@ namespace osu.Game.Screens.OnlinePlay Spacing = new Vector2(0, 2) }; - protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) => new DrawableRoomPlaylistItem(item) + protected sealed override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) => CreateDrawablePlaylistItem(item).With(d => { - SelectedItem = { BindTarget = SelectedItem }, - RequestDeletion = i => DeletionRequested?.Invoke(i), - AllowReordering = AllowReordering, - AllowDeletion = AllowDeletion, - AllowSelection = AllowSelection, - AllowShowingResults = AllowShowingResults, - ShowItemOwner = ShowItemOwners, - ShowResultsRequested = i => ShowResultsRequested?.Invoke(i) - }; + d.SelectedItem.BindTarget = SelectedItem; + d.RequestDeletion = i => DeletionRequested?.Invoke(i); + d.AllowReordering = AllowReordering; + d.AllowDeletion = AllowDeletion; + d.AllowSelection = AllowSelection; + d.AllowShowingResults = AllowShowingResults; + d.ShowItemOwner = ShowItemOwners; + d.ShowResultsRequested = i => ShowResultsRequested?.Invoke(i); + }); + + protected virtual DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new DrawableRoomPlaylistItem(item); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs index 7102738271..32d355d149 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Containers; using osu.Game.Online.Rooms; using osuTK; @@ -16,14 +15,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist /// public class MultiplayerHistoryList : DrawableRoomPlaylist { + public MultiplayerHistoryList() + { + ShowItemOwners = true; + } + protected override FillFlowContainer> CreateListFillFlowContainer() => new HistoryFillFlowContainer { Spacing = new Vector2(0, 2) }; - protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) - => base.CreateOsuDrawable(item).With(d => ((DrawableRoomPlaylistItem)d).ShowItemOwner = true); - private class HistoryFillFlowContainer : FillFlowContainer> { public override IEnumerable FlowingChildren => base.FlowingChildren.OfType>().OrderByDescending(item => item.Model.PlayedAt); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index 814ea48646..7dfee36895 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Containers; using osu.Game.Online.Rooms; using osuTK; @@ -18,14 +17,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist ///
public class MultiplayerQueueList : DrawableRoomPlaylist { + public MultiplayerQueueList() + { + ShowItemOwners = true; + } + protected override FillFlowContainer> CreateListFillFlowContainer() => new QueueFillFlowContainer { Spacing = new Vector2(0, 2) }; - protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) - => base.CreateOsuDrawable(item).With(d => ((DrawableRoomPlaylistItem)d).ShowItemOwner = true); - private class QueueFillFlowContainer : FillFlowContainer> { [Resolved(typeof(Room), nameof(Room.Playlist))] From 23332995d133672ec63e3c29e8859d58c5d8625a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Dec 2021 01:52:59 +0900 Subject: [PATCH 257/419] Invert naming of exposed actions --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs | 12 ++++++------ .../Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 4 ++-- .../Playlists/PlaylistsRoomSettingsPlaylist.cs | 2 +- .../OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 1abc7c47d6..8bd2daa2c3 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -26,12 +26,12 @@ namespace osu.Game.Screens.OnlinePlay /// /// Invoked when an item is requested to be deleted. /// - public Action DeletionRequested; + public Action RequestDeletion; /// /// Invoked when an item requests its results to be shown. /// - public Action ShowResultsRequested; + public Action RequestResults; private bool allowReordering; @@ -54,7 +54,7 @@ namespace osu.Game.Screens.OnlinePlay /// /// Whether to allow deleting items from the playlist. - /// If true, requests to delete items may be satisfied via . + /// If true, requests to delete items may be satisfied via . /// public bool AllowDeletion { @@ -90,7 +90,7 @@ namespace osu.Game.Screens.OnlinePlay /// /// Whether to allow items to request their results to be shown. - /// If true, requests to show the results may be satisfied via . + /// If true, requests to show the results may be satisfied via . /// public bool AllowShowingResults { @@ -134,13 +134,13 @@ namespace osu.Game.Screens.OnlinePlay protected sealed override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) => CreateDrawablePlaylistItem(item).With(d => { d.SelectedItem.BindTarget = SelectedItem; - d.RequestDeletion = i => DeletionRequested?.Invoke(i); + d.RequestDeletion = i => RequestDeletion?.Invoke(i); d.AllowReordering = AllowReordering; d.AllowDeletion = AllowDeletion; d.AllowSelection = AllowSelection; d.AllowShowingResults = AllowShowingResults; d.ShowItemOwner = ShowItemOwners; - d.ShowResultsRequested = i => ShowResultsRequested?.Invoke(i); + d.RequestResults = i => RequestResults?.Invoke(i); }); protected virtual DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new DrawableRoomPlaylistItem(item); diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index fca944e9b6..7a1c069df3 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay /// /// Invoked when this item requests its results to be shown. /// - public Action ShowResultsRequested; + public Action RequestResults; /// /// The currently-selected item, used to show a border around this item. @@ -410,7 +410,7 @@ namespace osu.Game.Screens.OnlinePlay { showResultsButton = new ShowResultsButton { - Action = () => ShowResultsRequested?.Invoke(Item), + Action = () => RequestResults?.Invoke(Item), Alpha = AllowShowingResults ? 1 : 0, }, Item.Beatmap.Value == null ? Empty() : new PlaylistDownloadButton(Item), diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsPlaylist.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsPlaylist.cs index bbe67e76e3..2fe215eef2 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsPlaylist.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists AllowReordering = true; AllowDeletion = true; - DeletionRequested = item => + RequestDeletion = item => { var nextItem = Items.GetNext(item); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index dd2d22b742..4114a5e9a0 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists SelectedItem = { BindTarget = SelectedItem }, AllowSelection = true, AllowShowingResults = true, - ShowResultsRequested = item => + RequestResults = item => { Debug.Assert(RoomId.Value != null); ParentScreen?.Push(new PlaylistsResultsScreen(null, RoomId.Value.Value, item, false)); From ce081c4acc6e02f8d22fac4d100050c1d40f0cac Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Dec 2021 02:01:11 +0900 Subject: [PATCH 258/419] Fix missing propagation of OwnerId in tests --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index cee6d8fe41..8ec073ff1e 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -62,6 +62,7 @@ namespace osu.Game.Online.Rooms public MultiplayerPlaylistItem(PlaylistItem item) { ID = item.ID; + OwnerID = item.OwnerID; BeatmapID = item.BeatmapID; BeatmapChecksum = item.Beatmap.Value?.MD5Hash ?? string.Empty; RulesetID = item.RulesetID; From c34c580ad4a7da49bfe3cc7a5f6bc44fc9e750fb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Dec 2021 02:12:33 +0900 Subject: [PATCH 259/419] Add client-side + interface implementation --- .../Online/TestSceneMultiplayerQueueList.cs | 177 ++++++++++++++++++ .../Multiplayer/IMultiplayerRoomServer.cs | 6 + .../Online/Multiplayer/MultiplayerClient.cs | 2 + .../Multiplayer/OnlineMultiplayerClient.cs | 8 + .../Match/Playlist/MultiplayerQueueList.cs | 43 +++++ .../Multiplayer/TestMultiplayerClient.cs | 30 +++ 6 files changed, 266 insertions(+) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs new file mode 100644 index 0000000000..d693ee60f0 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs @@ -0,0 +1,177 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Graphics.Containers; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.Multiplayer; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneMultiplayerQueueList : MultiplayerTestScene + { + private MultiplayerQueueList playlist; + + [Cached(typeof(UserLookupCache))] + private readonly TestUserLookupCache userLookupCache = new TestUserLookupCache(); + + private BeatmapManager beatmaps; + private RulesetStore rulesets; + private BeatmapSetInfo importedSet; + private BeatmapInfo importedBeatmap; + + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + } + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("create playlist", () => + { + Child = playlist = new MultiplayerQueueList + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(500, 300), + Items = { BindTarget = Client.APIRoom!.Playlist } + }; + }); + + AddStep("import beatmap", () => + { + beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0); + }); + + AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); + } + + [Test] + public void TestDeleteButtonHiddenWithSingleItem() + { + AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); + AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); + + assertDeleteButtonVisibility(0, false); + + addPlaylistItem(() => API.LocalUser.Value.OnlineID); + assertDeleteButtonVisibility(0, true); + + deleteItem(1); + assertDeleteButtonVisibility(0, false); + } + + [Test] + public void TestDeleteButtonHiddenInHostOnlyMode() + { + AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); + AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); + addPlaylistItem(() => 1234); + + AddStep("set host-only queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.HostOnly })); + AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.HostOnly); + + assertDeleteButtonVisibility(0, false); + } + + [Test] + public void TestOnlyItemOwnerHasDeleteButton() + { + AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); + AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); + + addPlaylistItem(() => API.LocalUser.Value.OnlineID); + assertDeleteButtonVisibility(0, true); + assertDeleteButtonVisibility(1, true); + + addPlaylistItem(() => 1234); + assertDeleteButtonVisibility(2, false); + } + + [Test] + public void TestNonOwnerDoesNotHaveDeleteButton() + { + AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); + AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); + + addPlaylistItem(() => 1234); + assertDeleteButtonVisibility(0, true); + assertDeleteButtonVisibility(1, false); + } + + [Test] + public void TestSelectedItemDoesNotHaveDeleteButton() + { + AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); + AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); + + addPlaylistItem(() => API.LocalUser.Value.OnlineID); + assertDeleteButtonVisibility(0, true); + + AddStep("set first playlist item as selected", () => playlist.SelectedItem.Value = playlist.Items[0]); + assertDeleteButtonVisibility(0, false); + } + + private void addPlaylistItem(Func userId) + { + long itemId = -1; + + AddStep("add playlist item", () => + { + MultiplayerPlaylistItem item = new MultiplayerPlaylistItem(new PlaylistItem + { + Beatmap = { Value = importedBeatmap }, + BeatmapID = importedBeatmap.OnlineID ?? -1, + }); + + Client.AddUserPlaylistItem(userId(), item); + + itemId = item.ID; + }); + + AddUntilStep("item arrived in playlist", () => playlist.ChildrenOfType>().Any(i => i.Model.ID == itemId)); + } + + private void deleteItem(int index) + { + OsuRearrangeableListItem item = null; + + AddStep($"move mouse to delete button {index}", () => + { + item = playlist.ChildrenOfType>().ElementAt(index); + InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0)); + }); + + AddStep("click", () => InputManager.Click(MouseButton.Left)); + + AddUntilStep("item removed from playlist", () => !playlist.ChildrenOfType>().Contains(item)); + } + + private void assertDeleteButtonVisibility(int index, bool visible) + => AddUntilStep($"delete button {index} {(visible ? "is" : "is not")} visible", + () => (playlist.ChildrenOfType().ElementAt(index).Alpha > 0) == visible); + } +} diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 3e84e4b904..65467e6ba9 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -82,5 +82,11 @@ namespace osu.Game.Online.Multiplayer /// /// The item to add. Task AddPlaylistItem(MultiplayerPlaylistItem item); + + /// + /// Removes an item from the playlist. + /// + /// The item to remove. + Task RemovePlaylistItem(long playlistItemId); } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 7e874495c8..34dc7ea5ea 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -335,6 +335,8 @@ namespace osu.Game.Online.Multiplayer public abstract Task AddPlaylistItem(MultiplayerPlaylistItem item); + public abstract Task RemovePlaylistItem(long playlistItemId); + Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state) { if (Room == null) diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 41687b54b0..7314603603 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -162,6 +162,14 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.AddPlaylistItem), item); } + public override Task RemovePlaylistItem(long playlistItemId) + { + if (!IsConnected.Value) + return Task.CompletedTask; + + return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); + } + protected override Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default) { return beatmapLookupCache.GetBeatmapAsync(beatmapId, cancellationToken); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index 7dfee36895..e74b2e8384 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -7,6 +7,8 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osuTK; @@ -27,6 +29,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist Spacing = new Vector2(0, 2) }; + protected override DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new QueuePlaylistItem(item); + private class QueueFillFlowContainer : FillFlowContainer> { [Resolved(typeof(Room), nameof(Room.Playlist))] @@ -40,5 +44,44 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist public override IEnumerable FlowingChildren => base.FlowingChildren.OfType>().OrderBy(item => item.Model.PlaylistOrder); } + + private class QueuePlaylistItem : DrawableRoomPlaylistItem + { + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } + + [Resolved(typeof(Room), nameof(Room.QueueMode))] + private Bindable queueMode { get; set; } + + [Resolved(typeof(Room), nameof(Room.Playlist))] + private BindableList playlist { get; set; } + + public QueuePlaylistItem(PlaylistItem item) + : base(item) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + RequestDeletion = item => multiplayerClient.RemovePlaylistItem(item.ID); + + playlist.BindCollectionChanged((_, __) => updateDeleteButtonVisibility()); + queueMode.BindValueChanged(_ => updateDeleteButtonVisibility()); + SelectedItem.BindValueChanged(_ => updateDeleteButtonVisibility(), true); + } + + private void updateDeleteButtonVisibility() + { + AllowDeletion = queueMode.Value != QueueMode.HostOnly + && playlist.Count > 1 + && Item.OwnerID == api.LocalUser.Value.OnlineID + && SelectedItem.Value != Item; + } + } } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index b3ea5bdc4a..1f4e031b40 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -339,6 +339,36 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task AddPlaylistItem(MultiplayerPlaylistItem item) => AddUserPlaylistItem(api.LocalUser.Value.OnlineID, item); + public async Task RemoveUserPlaylistItem(int userId, long playlistItemId) + { + Debug.Assert(Room != null); + Debug.Assert(APIRoom != null); + + if (Room.Settings.QueueMode == QueueMode.HostOnly) + throw new InvalidOperationException("Items cannot be removed in host-only mode."); + + if (Room.Playlist.Count == 1) + throw new InvalidOperationException("The singular item in the room cannot be removed."); + + var item = serverSidePlaylist.Find(i => i.ID == playlistItemId); + + if (item == null) + throw new InvalidOperationException("Item does not exist in the room."); + + if (item == currentItem) + throw new InvalidOperationException("The room's current item cannot be removed."); + + if (item.OwnerID != userId) + throw new InvalidOperationException("Attempted to remove an item which is not owned by the user."); + + serverSidePlaylist.Remove(item); + await ((IMultiplayerClient)this).PlaylistItemRemoved(playlistItemId).ConfigureAwait(false); + + await updateCurrentItem(Room).ConfigureAwait(false); + } + + public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, playlistItemId); + protected override Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default) { IBeatmapSetInfo? set = roomManager.ServerSideRooms.SelectMany(r => r.Playlist) From 8398f86440ae0f941ea07e9dc51366d6e135db16 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Dec 2021 04:02:16 +0900 Subject: [PATCH 260/419] Don't consider expired items in visibility check --- .../Match/Playlist/MultiplayerQueueList.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index e74b2e8384..72bcf7d343 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -29,7 +29,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist Spacing = new Vector2(0, 2) }; - protected override DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new QueuePlaylistItem(item); + protected override DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new QueuePlaylistItem(item) + { + Items = { BindTarget = Items } + }; private class QueueFillFlowContainer : FillFlowContainer> { @@ -47,6 +50,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist private class QueuePlaylistItem : DrawableRoomPlaylistItem { + public readonly IBindableList Items = new BindableList(); + [Resolved] private IAPIProvider api { get; set; } @@ -56,9 +61,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist [Resolved(typeof(Room), nameof(Room.QueueMode))] private Bindable queueMode { get; set; } - [Resolved(typeof(Room), nameof(Room.Playlist))] - private BindableList playlist { get; set; } - public QueuePlaylistItem(PlaylistItem item) : base(item) { @@ -70,7 +72,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist RequestDeletion = item => multiplayerClient.RemovePlaylistItem(item.ID); - playlist.BindCollectionChanged((_, __) => updateDeleteButtonVisibility()); + Items.BindCollectionChanged((_, __) => updateDeleteButtonVisibility()); queueMode.BindValueChanged(_ => updateDeleteButtonVisibility()); SelectedItem.BindValueChanged(_ => updateDeleteButtonVisibility(), true); } @@ -78,7 +80,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist private void updateDeleteButtonVisibility() { AllowDeletion = queueMode.Value != QueueMode.HostOnly - && playlist.Count > 1 + && Items.Count > 1 && Item.OwnerID == api.LocalUser.Value.OnlineID && SelectedItem.Value != Item; } From 4df2047a581d70687e5854209285ebdfadf2ca3f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Dec 2021 04:12:24 +0900 Subject: [PATCH 261/419] Prevent removal of expired items in TestMultiplayerClient --- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 1f4e031b40..c82b748a2b 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -361,6 +361,9 @@ namespace osu.Game.Tests.Visual.Multiplayer if (item.OwnerID != userId) throw new InvalidOperationException("Attempted to remove an item which is not owned by the user."); + if (item.Expired) + throw new InvalidOperationException("Attempted to remove an item which has already been played"); + serverSidePlaylist.Remove(item); await ((IMultiplayerClient)this).PlaylistItemRemoved(playlistItemId).ConfigureAwait(false); From 80b2768a5f6c2d18cdd6225becc9a55cf8172210 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Dec 2021 04:18:53 +0900 Subject: [PATCH 262/419] Mirror recent server-side changes --- .../Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index c82b748a2b..f151add430 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -347,9 +347,6 @@ namespace osu.Game.Tests.Visual.Multiplayer if (Room.Settings.QueueMode == QueueMode.HostOnly) throw new InvalidOperationException("Items cannot be removed in host-only mode."); - if (Room.Playlist.Count == 1) - throw new InvalidOperationException("The singular item in the room cannot be removed."); - var item = serverSidePlaylist.Find(i => i.ID == playlistItemId); if (item == null) @@ -362,7 +359,7 @@ namespace osu.Game.Tests.Visual.Multiplayer throw new InvalidOperationException("Attempted to remove an item which is not owned by the user."); if (item.Expired) - throw new InvalidOperationException("Attempted to remove an item which has already been played"); + throw new InvalidOperationException("Attempted to remove an item which has already been played."); serverSidePlaylist.Remove(item); await ((IMultiplayerClient)this).PlaylistItemRemoved(playlistItemId).ConfigureAwait(false); @@ -471,11 +468,12 @@ namespace osu.Game.Tests.Visual.Multiplayer await updatePlaylistOrder(Room).ConfigureAwait(false); } + private IEnumerable upcomingItems => serverSidePlaylist.Where(i => !i.Expired).OrderBy(i => i.PlaylistOrder); + private async Task updateCurrentItem(MultiplayerRoom room, bool notify = true) { // Pick the next non-expired playlist item by playlist order, or default to the most-recently-expired item. - MultiplayerPlaylistItem nextItem = serverSidePlaylist.Where(i => !i.Expired).OrderBy(i => i.PlaylistOrder).FirstOrDefault() - ?? serverSidePlaylist.OrderByDescending(i => i.PlayedAt).First(); + MultiplayerPlaylistItem nextItem = upcomingItems.FirstOrDefault() ?? serverSidePlaylist.OrderByDescending(i => i.PlayedAt).First(); currentIndex = serverSidePlaylist.IndexOf(nextItem); From aec36adf6ce2fff49265a6ca3cc6bfb50714b518 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Dec 2021 04:20:23 +0900 Subject: [PATCH 263/419] Fix test failures --- .../Multiplayer/TestSceneDrawableRoomPlaylist.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 269cd15a0f..c9a5552f39 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("import beatmap", () => manager.Import(beatmap.BeatmapSet).Wait()); - createPlaylist(beatmap); + createPlaylistWithBeatmaps(beatmap); assertDownloadButtonVisible(false); @@ -159,7 +159,7 @@ namespace osu.Game.Tests.Visual.Multiplayer var byChecksum = CreateAPIBeatmap(); byChecksum.Checksum = "1337"; // Some random checksum that does not exist locally. - createPlaylist(byOnlineId, byChecksum); + createPlaylistWithBeatmaps(byOnlineId, byChecksum); AddAssert("download buttons shown", () => playlist.ChildrenOfType().All(d => d.IsPresent)); } @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Multiplayer beatmap.BeatmapSet.HasExplicitContent = true; - createPlaylist(beatmap); + createPlaylistWithBeatmaps(beatmap); } [Test] @@ -246,7 +246,7 @@ namespace osu.Game.Tests.Visual.Multiplayer => AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible", () => (playlist.ChildrenOfType().ElementAt(2 + index * 2).Alpha > 0) == visible); - private void createPlaylist(Action setupPlaylist) + private void createPlaylist(Action setupPlaylist = null) { AddStep("create playlist", () => { @@ -257,6 +257,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Size = new Vector2(500, 300) }; + setupPlaylist?.Invoke(playlist); + for (int i = 0; i < 20; i++) { playlist.Items.Add(new PlaylistItem @@ -292,7 +294,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded)); } - private void createPlaylist(params IBeatmapInfo[] beatmaps) + private void createPlaylistWithBeatmaps(params IBeatmapInfo[] beatmaps) { AddStep("create playlist", () => { From a32492cdd5614f9d88c4d3e04b5222a10002b0ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Nov 2021 15:27:41 +0100 Subject: [PATCH 264/419] Duplicate `BeatmapCard{-> Extra}` as blueprint for extra card size --- .../Visual/Beatmaps/TestSceneBeatmapCard.cs | 14 + .../Drawables/Cards/BeatmapCardExtra.cs | 430 ++++++++++++++++++ 2 files changed, 444 insertions(+) create mode 100644 osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs index f835d21603..04aea4ac9c 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs @@ -262,5 +262,19 @@ namespace osu.Game.Tests.Visual.Beatmaps }); AddToggleStep("disable/enable expansion", disabled => this.ChildrenOfType().ForEach(card => card.Expanded.Disabled = disabled)); } + + [Test] + public void TestExtra() + { + createTestCase(beatmapSetInfo => new BeatmapCardExtra(beatmapSetInfo)); + + AddToggleStep("toggle expanded state", expanded => + { + var card = this.ChildrenOfType().Last(); + if (!card.Expanded.Disabled) + card.Expanded.Value = expanded; + }); + AddToggleStep("disable/enable expansion", disabled => this.ChildrenOfType().ForEach(card => card.Expanded.Disabled = disabled)); + } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs new file mode 100644 index 0000000000..f244e912a1 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -0,0 +1,430 @@ +// 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.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Beatmaps.Drawables.Cards.Buttons; +using osu.Game.Beatmaps.Drawables.Cards.Statistics; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.BeatmapSet; +using osuTK; +using osu.Game.Resources.Localisation.Web; +using DownloadButton = osu.Game.Beatmaps.Drawables.Cards.Buttons.DownloadButton; + +namespace osu.Game.Beatmaps.Drawables.Cards +{ + public class BeatmapCardExtra : OsuClickableContainer + { + private const float width = 408; + private const float height = 100; + private const float icon_area_width = 30; + + public Bindable Expanded { get; } = new BindableBool(); + + private readonly APIBeatmapSet beatmapSet; + private readonly Bindable favouriteState; + + private readonly BeatmapDownloadTracker downloadTracker; + + private BeatmapCardContent content = null!; + + private BeatmapCardThumbnail thumbnail = null!; + + private Container rightAreaBackground = null!; + private Container rightAreaButtons = null!; + + private Container mainContent = null!; + private BeatmapCardContentBackground mainContentBackground = null!; + private FillFlowContainer statisticsContainer = null!; + + private FillFlowContainer idleBottomContent = null!; + private BeatmapCardDownloadProgressBar downloadProgressBar = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public BeatmapCardExtra(APIBeatmapSet beatmapSet) + : base(HoverSampleSet.Submit) + { + this.beatmapSet = beatmapSet; + favouriteState = new Bindable(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount)); + downloadTracker = new BeatmapDownloadTracker(beatmapSet); + } + + [BackgroundDependencyLoader(true)] + private void load(BeatmapSetOverlay? beatmapSetOverlay) + { + Width = width; + Height = height; + + FillFlowContainer leftIconArea; + GridContainer titleContainer; + GridContainer artistContainer; + + InternalChild = content = new BeatmapCardContent(height) + { + MainContent = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + downloadTracker, + rightAreaBackground = new Container + { + RelativeSizeAxes = Axes.Y, + Width = icon_area_width + 2 * BeatmapCard.CORNER_RADIUS, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + // workaround for masking artifacts at the top & bottom of card, + // which become especially visible on downloaded beatmaps (when the icon area has a lime background). + Padding = new MarginPadding { Vertical = 1 }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White + }, + }, + thumbnail = new BeatmapCardThumbnail(beatmapSet) + { + Name = @"Left (icon) area", + Size = new Vector2(height), + Padding = new MarginPadding { Right = BeatmapCard.CORNER_RADIUS }, + Child = leftIconArea = new FillFlowContainer + { + Margin = new MarginPadding(5), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(1) + } + }, + new Container + { + Name = @"Right (button) area", + Width = 30, + RelativeSizeAxes = Axes.Y, + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + Padding = new MarginPadding { Vertical = 17.5f }, + Child = rightAreaButtons = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new BeatmapCardIconButton[] + { + new FavouriteButton(beatmapSet) + { + Current = favouriteState, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }, + new DownloadButton(beatmapSet) + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + State = { BindTarget = downloadTracker.State } + }, + new GoToBeatmapButton(beatmapSet) + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + State = { BindTarget = downloadTracker.State } + } + } + } + }, + mainContent = new Container + { + Name = @"Main content", + X = height - BeatmapCard.CORNER_RADIUS, + Height = height, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Masking = true, + Children = new Drawable[] + { + mainContentBackground = new BeatmapCardContentBackground(beatmapSet) + { + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = 10, + Vertical = 4 + }, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + titleContainer = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new[] + { + new OsuSpriteText + { + Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title), + Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + Truncate = true + }, + Empty() + } + } + }, + artistContainer = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new[] + { + new OsuSpriteText + { + Text = createArtistText(), + Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + Truncate = true + }, + Empty() + }, + } + }, + new LinkFlowContainer(s => + { + s.Shadow = false; + s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold); + }).With(d => + { + d.AutoSizeAxes = Axes.Both; + d.Margin = new MarginPadding { Top = 2 }; + d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); + d.AddUserLink(beatmapSet.Author); + }), + } + }, + new Container + { + Name = @"Bottom content", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Padding = new MarginPadding + { + Horizontal = 10, + Vertical = 4 + }, + Children = new Drawable[] + { + idleBottomContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 3), + AlwaysPresent = true, + Children = new Drawable[] + { + statisticsContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Alpha = 0, + AlwaysPresent = true, + ChildrenEnumerable = createStatistics() + }, + new BeatmapCardExtraInfoRow(beatmapSet) + { + Hovered = _ => + { + content.ScheduleShow(); + return false; + }, + Unhovered = _ => + { + // This hide should only trigger if the expanded content has not shown yet. + // ie. if the user has not shown intent to want to see it (quickly moved over the info row area). + if (!Expanded.Value) + content.ScheduleHide(); + } + } + } + }, + downloadProgressBar = new BeatmapCardDownloadProgressBar + { + RelativeSizeAxes = Axes.X, + Height = 6, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = { BindTarget = downloadTracker.State }, + Progress = { BindTarget = downloadTracker.Progress } + } + } + } + } + } + } + }, + ExpandedContent = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 10, Vertical = 13 }, + Child = new BeatmapCardDifficultyList(beatmapSet) + }, + Expanded = { BindTarget = Expanded } + }; + + if (beatmapSet.HasVideo) + leftIconArea.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) }); + + if (beatmapSet.HasStoryboard) + leftIconArea.Add(new IconPill(FontAwesome.Solid.Image) { IconSize = new Vector2(20) }); + + if (beatmapSet.HasExplicitContent) + { + titleContainer.Content[0][1] = new ExplicitContentBeatmapPill + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding { Left = 5 } + }; + } + + if (beatmapSet.TrackId != null) + { + artistContainer.Content[0][1] = new FeaturedArtistBeatmapPill + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding { Left = 5 } + }; + } + + Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapSet.OnlineID); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + downloadTracker.State.BindValueChanged(_ => updateState()); + Expanded.BindValueChanged(_ => updateState(), true); + FinishTransforms(true); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + content.ScheduleHide(); + + updateState(); + base.OnHoverLost(e); + } + + private LocalisableString createArtistText() + { + var romanisableArtist = new RomanisableString(beatmapSet.ArtistUnicode, beatmapSet.Artist); + return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist); + } + + private IEnumerable createStatistics() + { + if (beatmapSet.HypeStatus != null) + yield return new HypesStatistic(beatmapSet.HypeStatus); + + // web does not show nominations unless hypes are also present. + // see: https://github.com/ppy/osu-web/blob/8ed7d071fd1d3eaa7e43cf0e4ff55ca2fef9c07c/resources/assets/lib/beatmapset-panel.tsx#L443 + if (beatmapSet.HypeStatus != null && beatmapSet.NominationStatus != null) + yield return new NominationsStatistic(beatmapSet.NominationStatus); + + yield return new FavouritesStatistic(beatmapSet) { Current = favouriteState }; + yield return new PlayCountStatistic(beatmapSet); + + var dateStatistic = BeatmapCardDateStatistic.CreateFor(beatmapSet); + if (dateStatistic != null) + yield return dateStatistic; + } + + private void updateState() + { + bool showDetails = IsHovered || Expanded.Value; + + float targetWidth = width - height; + if (showDetails) + targetWidth = targetWidth - icon_area_width + BeatmapCard.CORNER_RADIUS; + + thumbnail.Dimmed.Value = showDetails; + + // Scale value is intentionally chosen to fit in the spacing of listing displays, as to not overlap horizontally with adjacent cards. + // This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left. + content.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint); + + mainContent.ResizeWidthTo(targetWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + mainContentBackground.Dimmed.Value = showDetails; + + statisticsContainer.FadeTo(showDetails ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + rightAreaBackground.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + rightAreaButtons.FadeTo(showDetails ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + foreach (var button in rightAreaButtons) + { + button.IdleColour = downloadTracker.State.Value != DownloadState.LocallyAvailable ? colourProvider.Light1 : colourProvider.Background3; + button.HoverColour = downloadTracker.State.Value != DownloadState.LocallyAvailable ? colourProvider.Content1 : colourProvider.Foreground1; + } + + bool showProgress = downloadTracker.State.Value == DownloadState.Downloading || downloadTracker.State.Value == DownloadState.Importing; + + idleBottomContent.FadeTo(showProgress ? 0 : 1, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + downloadProgressBar.FadeTo(showProgress ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + } + } +} From 61e04f75cc7ad4c5966e605525679e7b28f23ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Nov 2021 15:41:09 +0100 Subject: [PATCH 265/419] Resize extra card to design size --- osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index f244e912a1..7e79bded5d 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -30,8 +30,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards { public class BeatmapCardExtra : OsuClickableContainer { - private const float width = 408; - private const float height = 100; + private const float width = 475; + private const float height = 140; private const float icon_area_width = 30; public Bindable Expanded { get; } = new BindableBool(); From 419fee1380ad94e068b417e7ea3fc1a5244c078e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Nov 2021 15:46:01 +0100 Subject: [PATCH 266/419] Move mapper link to bottom content --- .../Drawables/Cards/BeatmapCardExtra.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index 7e79bded5d..f42c96be7c 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -229,17 +229,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards }, } }, - new LinkFlowContainer(s => - { - s.Shadow = false; - s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold); - }).With(d => - { - d.AutoSizeAxes = Axes.Both; - d.Margin = new MarginPadding { Top = 2 }; - d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); - d.AddUserLink(beatmapSet.Author); - }), } }, new Container @@ -265,6 +254,17 @@ namespace osu.Game.Beatmaps.Drawables.Cards AlwaysPresent = true, Children = new Drawable[] { + new LinkFlowContainer(s => + { + s.Shadow = false; + s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold); + }).With(d => + { + d.AutoSizeAxes = Axes.Both; + d.Margin = new MarginPadding { Top = 2 }; + d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); + d.AddUserLink(beatmapSet.Author); + }), statisticsContainer = new FillFlowContainer { RelativeSizeAxes = Axes.X, From 3ecfaa532cb270e36e96344b7f88488105e6b15c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Nov 2021 15:47:52 +0100 Subject: [PATCH 267/419] Add source field to extra beatmap card --- osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs | 1 + osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs index 04aea4ac9c..2a308dd0d4 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs @@ -97,6 +97,7 @@ namespace osu.Game.Tests.Visual.Beatmaps var longName = CreateAPIBeatmapSet(Ruleset.Value); longName.Title = longName.TitleUnicode = "this track has an incredibly and implausibly long title"; longName.Artist = longName.ArtistUnicode = "and this artist! who would have thunk it. it's really such a long name."; + longName.Source = "wow. even the source field has an impossibly long string in it. this really takes the cake, doesn't it?"; longName.HasExplicitContent = true; longName.TrackId = 444; diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index f42c96be7c..1926614c8f 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -229,6 +229,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards }, } }, + new OsuSpriteText + { + RelativeSizeAxes = Axes.X, + Truncate = true, + Text = beatmapSet.Source, + Shadow = false, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), + Colour = colourProvider.Content2 + }, } }, new Container From 2d739c95ea133a9b0068a283a7296700a6377f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Nov 2021 16:01:35 +0100 Subject: [PATCH 268/419] Lay out extra card statistics in grid as per design --- .../Drawables/Cards/BeatmapCardExtra.cs | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index 1926614c8f..663bb26919 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -3,7 +3,6 @@ #nullable enable -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -50,7 +49,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards private Container mainContent = null!; private BeatmapCardContentBackground mainContentBackground = null!; - private FillFlowContainer statisticsContainer = null!; + private GridContainer statisticsContainer = null!; private FillFlowContainer idleBottomContent = null!; private BeatmapCardDownloadProgressBar downloadProgressBar = null!; @@ -274,15 +273,26 @@ namespace osu.Game.Beatmaps.Drawables.Cards d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); d.AddUserLink(beatmapSet.Author); }), - statisticsContainer = new FillFlowContainer + statisticsContainer = new GridContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Alpha = 0, - AlwaysPresent = true, - ChildrenEnumerable = createStatistics() + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize) + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension() + }, + Content = new[] + { + new Drawable[3], + new Drawable[3] + } }, new BeatmapCardExtraInfoRow(beatmapSet) { @@ -352,6 +362,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards }; } + createStatistics(); + Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapSet.OnlineID); } @@ -384,22 +396,32 @@ namespace osu.Game.Beatmaps.Drawables.Cards return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist); } - private IEnumerable createStatistics() + private void createStatistics() { + BeatmapCardStatistic withMargin(BeatmapCardStatistic original) + { + original.Margin = new MarginPadding { Right = 10 }; + return original; + } + + statisticsContainer.Content[0][0] = withMargin(new FavouritesStatistic(beatmapSet) + { + Current = favouriteState, + }); + + statisticsContainer.Content[1][0] = withMargin(new PlayCountStatistic(beatmapSet)); + if (beatmapSet.HypeStatus != null) - yield return new HypesStatistic(beatmapSet.HypeStatus); + statisticsContainer.Content[0][1] = withMargin(new HypesStatistic(beatmapSet.HypeStatus)); // web does not show nominations unless hypes are also present. // see: https://github.com/ppy/osu-web/blob/8ed7d071fd1d3eaa7e43cf0e4ff55ca2fef9c07c/resources/assets/lib/beatmapset-panel.tsx#L443 if (beatmapSet.HypeStatus != null && beatmapSet.NominationStatus != null) - yield return new NominationsStatistic(beatmapSet.NominationStatus); - - yield return new FavouritesStatistic(beatmapSet) { Current = favouriteState }; - yield return new PlayCountStatistic(beatmapSet); + statisticsContainer.Content[1][1] = withMargin(new NominationsStatistic(beatmapSet.NominationStatus)); var dateStatistic = BeatmapCardDateStatistic.CreateFor(beatmapSet); if (dateStatistic != null) - yield return dateStatistic; + statisticsContainer.Content[0][2] = withMargin(dateStatistic); } private void updateState() @@ -419,8 +441,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards mainContent.ResizeWidthTo(targetWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); mainContentBackground.Dimmed.Value = showDetails; - statisticsContainer.FadeTo(showDetails ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - rightAreaBackground.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); rightAreaButtons.FadeTo(showDetails ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); From 083ee92deea8f018032fcaa9d111f2192dd76a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Nov 2021 16:09:00 +0100 Subject: [PATCH 269/419] Adjust button vertical padding --- osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index 663bb26919..f47e1a7eab 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -121,7 +121,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.Y, Origin = Anchor.TopRight, Anchor = Anchor.TopRight, - Padding = new MarginPadding { Vertical = 17.5f }, + Padding = new MarginPadding { Vertical = 35 }, Child = rightAreaButtons = new Container { RelativeSizeAxes = Axes.Both, From 1a0945dabadbe14990004cd8dfbbb02a8d8271eb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Dec 2021 14:11:29 +0900 Subject: [PATCH 270/419] Siplify condition, allow host to always remove items --- .../Online/TestSceneMultiplayerQueueList.cs | 80 +++++++++---------- .../Match/Playlist/MultiplayerQueueList.cs | 17 ++-- 2 files changed, 43 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs index d693ee60f0..19e5624a04 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs @@ -13,6 +13,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; @@ -55,6 +56,7 @@ namespace osu.Game.Tests.Visual.Online Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(500, 300), + SelectedItem = { BindTarget = Client.CurrentMatchPlayingItem }, Items = { BindTarget = Client.APIRoom!.Playlist } }; }); @@ -70,69 +72,59 @@ namespace osu.Game.Tests.Visual.Online } [Test] - public void TestDeleteButtonHiddenWithSingleItem() - { - AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); - AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); - - assertDeleteButtonVisibility(0, false); - - addPlaylistItem(() => API.LocalUser.Value.OnlineID); - assertDeleteButtonVisibility(0, true); - - deleteItem(1); - assertDeleteButtonVisibility(0, false); - } - - [Test] - public void TestDeleteButtonHiddenInHostOnlyMode() - { - AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); - AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); - addPlaylistItem(() => 1234); - - AddStep("set host-only queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.HostOnly })); - AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.HostOnly); - - assertDeleteButtonVisibility(0, false); - } - - [Test] - public void TestOnlyItemOwnerHasDeleteButton() + public void TestDeleteButtonAlwaysVisibleForHost() { AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); addPlaylistItem(() => API.LocalUser.Value.OnlineID); - assertDeleteButtonVisibility(0, true); assertDeleteButtonVisibility(1, true); + addPlaylistItem(() => 1234); + assertDeleteButtonVisibility(2, true); + } + [Test] + public void TestDeleteButtonOnlyVisibleForItemOwnerIfNotHost() + { + AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); + AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); + + AddStep("join other user", () => Client.AddUser(new APIUser { Id = 1234 })); + AddStep("set other user as host", () => Client.TransferHost(1234)); + + addPlaylistItem(() => API.LocalUser.Value.OnlineID); + assertDeleteButtonVisibility(1, true); addPlaylistItem(() => 1234); assertDeleteButtonVisibility(2, false); + + AddStep("set local user as host", () => Client.TransferHost(API.LocalUser.Value.OnlineID)); + assertDeleteButtonVisibility(1, true); + assertDeleteButtonVisibility(2, true); } [Test] - public void TestNonOwnerDoesNotHaveDeleteButton() + public void TestCurrentItemDoesNotHaveDeleteButton() { AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); - addPlaylistItem(() => 1234); - assertDeleteButtonVisibility(0, true); - assertDeleteButtonVisibility(1, false); - } - - [Test] - public void TestSelectedItemDoesNotHaveDeleteButton() - { - AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); - AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); + assertDeleteButtonVisibility(0, false); addPlaylistItem(() => API.LocalUser.Value.OnlineID); - assertDeleteButtonVisibility(0, true); - - AddStep("set first playlist item as selected", () => playlist.SelectedItem.Value = playlist.Items[0]); assertDeleteButtonVisibility(0, false); + assertDeleteButtonVisibility(1, true); + + // Run through gameplay. + AddStep("set state to ready", () => Client.ChangeUserState(API.LocalUser.Value.Id, MultiplayerUserState.Ready)); + AddUntilStep("local state is ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready); + AddStep("start match", () => Client.StartMatch()); + AddUntilStep("match started", () => Client.LocalUser?.State == MultiplayerUserState.WaitingForLoad); + AddStep("set state to loaded", () => Client.ChangeUserState(API.LocalUser.Value.Id, MultiplayerUserState.Loaded)); + AddUntilStep("local state is playing", () => Client.LocalUser?.State == MultiplayerUserState.Playing); + AddStep("set state to finished play", () => Client.ChangeUserState(API.LocalUser.Value.Id, MultiplayerUserState.FinishedPlay)); + AddUntilStep("local state is results", () => Client.LocalUser?.State == MultiplayerUserState.Results); + + assertDeleteButtonVisibility(1, false); } private void addPlaylistItem(Func userId) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index 72bcf7d343..8832c9bd60 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osuTK; @@ -29,10 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist Spacing = new Vector2(0, 2) }; - protected override DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new QueuePlaylistItem(item) - { - Items = { BindTarget = Items } - }; + protected override DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new QueuePlaylistItem(item); private class QueueFillFlowContainer : FillFlowContainer> { @@ -50,14 +48,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist private class QueuePlaylistItem : DrawableRoomPlaylistItem { - public readonly IBindableList Items = new BindableList(); - [Resolved] private IAPIProvider api { get; set; } [Resolved] private MultiplayerClient multiplayerClient { get; set; } + [Resolved(typeof(Room), nameof(Room.Host))] + private Bindable host { get; set; } + [Resolved(typeof(Room), nameof(Room.QueueMode))] private Bindable queueMode { get; set; } @@ -72,16 +71,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist RequestDeletion = item => multiplayerClient.RemovePlaylistItem(item.ID); - Items.BindCollectionChanged((_, __) => updateDeleteButtonVisibility()); + host.BindValueChanged(_ => updateDeleteButtonVisibility()); queueMode.BindValueChanged(_ => updateDeleteButtonVisibility()); SelectedItem.BindValueChanged(_ => updateDeleteButtonVisibility(), true); } private void updateDeleteButtonVisibility() { - AllowDeletion = queueMode.Value != QueueMode.HostOnly - && Items.Count > 1 - && Item.OwnerID == api.LocalUser.Value.OnlineID + AllowDeletion = (Item.OwnerID == api.LocalUser.Value.OnlineID || multiplayerClient.IsHost) && SelectedItem.Value != Item; } } From 17d676200bd2ce4c35d2a5642632b59169dc69fb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Dec 2021 17:33:36 +0900 Subject: [PATCH 271/419] Xmldoc fixes from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 8bd2daa2c3..5e180afcf9 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -13,13 +13,13 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay { /// - /// A list scrollable list which displays the s in a . + /// A scrollable list which displays the s in a . /// public class DrawableRoomPlaylist : OsuRearrangeableListContainer { /// - /// The currently-selected item, used to show a border around items. - /// May be updated by playlist items if is true. + /// The currently-selected item. Selection is visually represented with a border. + /// May be updated by clicking playlist items if is true. /// public readonly Bindable SelectedItem = new Bindable(); From 0963b0045303887db0e11e0ef01728ac709adddc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Dec 2021 17:33:59 +0900 Subject: [PATCH 272/419] Disallow item selection in playlists song select --- .../Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs index c29b8db26c..89842e933b 100644 --- a/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs +++ b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs @@ -46,8 +46,7 @@ namespace osu.Game.Screens.OnlinePlay.Components Padding = new MarginPadding { Bottom = 10 }, Child = playlist = new PlaylistsRoomSettingsPlaylist { - RelativeSizeAxes = Axes.Both, - AllowSelection = true, + RelativeSizeAxes = Axes.Both } } }, From dfe19f350968d3cbf490690261ea940ccb80fa80 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Dec 2021 17:48:09 +0900 Subject: [PATCH 273/419] Minor code reformatting --- .../TestSceneDrawableRoomPlaylist.cs | 27 +++++++++++--- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 36 +++++++++---------- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index c9a5552f39..f6c15f314e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -62,7 +62,11 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestEditable() { - createPlaylist(p => p.AllowReordering = p.AllowDeletion = true); + createPlaylist(p => + { + p.AllowReordering = true; + p.AllowDeletion = true; + }); moveToItem(0); assertHandleVisibility(0, true); @@ -75,7 +79,12 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestMarkInvalid() { - createPlaylist(p => p.AllowReordering = p.AllowDeletion = p.AllowSelection = true); + createPlaylist(p => + { + p.AllowReordering = true; + p.AllowDeletion = true; + p.AllowSelection = true; + }); AddStep("mark item 0 as invalid", () => playlist.Items[0].MarkInvalid()); @@ -102,7 +111,12 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestEditableSelectable() { - createPlaylist(p => p.AllowReordering = p.AllowDeletion = p.AllowSelection = true); + createPlaylist(p => + { + p.AllowReordering = true; + p.AllowDeletion = true; + p.AllowSelection = true; + }); moveToItem(0); assertHandleVisibility(0, true); @@ -116,7 +130,12 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestSelectionNotLostAfterRearrangement() { - createPlaylist(p => p.AllowReordering = p.AllowDeletion = p.AllowSelection = true); + createPlaylist(p => + { + p.AllowReordering = true; + p.AllowDeletion = true; + p.AllowSelection = true; + }); moveToItem(0); AddStep("click", () => InputManager.Click(MouseButton.Left)); diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 7a1c069df3..9ad43fced5 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -38,7 +38,8 @@ namespace osu.Game.Screens.OnlinePlay public class DrawableRoomPlaylistItem : OsuRearrangeableListItem { public const float HEIGHT = 50; - public const float ICON_HEIGHT = 34; + + private const float icon_height = 34; /// /// Invoked when this item requests to be deleted. @@ -238,7 +239,7 @@ namespace osu.Game.Screens.OnlinePlay } if (Item.Beatmap.Value != null) - difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset.Value, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(ICON_HEIGHT) }; + difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset.Value, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(icon_height) }; else difficultyIconContainer.Clear(); @@ -392,7 +393,7 @@ namespace osu.Game.Screens.OnlinePlay { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(ICON_HEIGHT), + Size = new Vector2(icon_height), Margin = new MarginPadding { Right = 8 }, Masking = true, CornerRadius = 4, @@ -405,22 +406,21 @@ namespace osu.Game.Screens.OnlinePlay }; } - private IEnumerable createButtons() => - new[] + private IEnumerable createButtons() => new[] + { + showResultsButton = new ShowResultsButton { - showResultsButton = new ShowResultsButton - { - Action = () => RequestResults?.Invoke(Item), - Alpha = AllowShowingResults ? 1 : 0, - }, - Item.Beatmap.Value == null ? Empty() : new PlaylistDownloadButton(Item), - removeButton = new PlaylistRemoveButton - { - Size = new Vector2(30, 30), - Alpha = AllowDeletion ? 1 : 0, - Action = () => RequestDeletion?.Invoke(Item), - }, - }; + Action = () => RequestResults?.Invoke(Item), + Alpha = AllowShowingResults ? 1 : 0, + }, + Item.Beatmap.Value == null ? Empty() : new PlaylistDownloadButton(Item), + removeButton = new PlaylistRemoveButton + { + Size = new Vector2(30, 30), + Alpha = AllowDeletion ? 1 : 0, + Action = () => RequestDeletion?.Invoke(Item), + }, + }; public class PlaylistRemoveButton : GrayButton { From e7e61cd9ab8b882ccb99325960e2b6677b972548 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Dec 2021 18:50:53 +0900 Subject: [PATCH 274/419] Fix potential crash due to children being mutated after disposal This is a bit of an unfortunate edge case where the unbind-on-disposal doesn't help, since the binding is happening in BDL, and the usage is in a nested `LoadComponentAsync` call. Combine those and you have a recipe for disaster. --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 25 ++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index e4cf9bd868..d292b7114f 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -330,6 +330,21 @@ namespace osu.Game.Screens.Select addInfoLabels(); } + protected override void LoadComplete() + { + base.LoadComplete(); + + mods.BindValueChanged(m => + { + settingChangeTracker?.Dispose(); + + refreshBPMLabel(); + + settingChangeTracker = new ModSettingChangeTracker(m.NewValue); + settingChangeTracker.SettingChanged += _ => refreshBPMLabel(); + }, true); + } + private void setMetadata(string source) { ArtistLabel.Text = artistBinding.Value; @@ -360,16 +375,6 @@ namespace osu.Game.Screens.Select Children = getRulesetInfoLabels() } }; - - mods.BindValueChanged(m => - { - settingChangeTracker?.Dispose(); - - refreshBPMLabel(); - - settingChangeTracker = new ModSettingChangeTracker(m.NewValue); - settingChangeTracker.SettingChanged += _ => refreshBPMLabel(); - }, true); } private InfoLabel[] getRulesetInfoLabels() From a3b53ac2f68a176f8406740b6fe486ac420a6ac4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Dec 2021 18:58:47 +0900 Subject: [PATCH 275/419] Change comparison to match in all locations --- osu.Game/Screens/Play/PlayerLoader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index dfc3c2b61d..60843acb4f 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -505,9 +505,9 @@ namespace osu.Game.Screens.Play volumeOverlay.IsMuted.Value = false; // Check values before resetting, as the user may have only had mute enabled, in which case we might not need to adjust volumes. - if (audioManager.Volume.Value < volume_requirement) + if (audioManager.Volume.Value <= volume_requirement) audioManager.Volume.SetDefault(); - if (audioManager.VolumeTrack.Value < volume_requirement) + if (audioManager.VolumeTrack.Value <= volume_requirement) audioManager.VolumeTrack.SetDefault(); return true; From f9af239ed95153275b66310f3caf67215a5ae73e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Dec 2021 23:46:03 +0900 Subject: [PATCH 276/419] Cleanup duplicated classes in DrawableRoomPlaylistItem --- .../TestSceneDrawableRoomPlaylist.cs | 12 ++++- .../TestScenePlaylistsRoomSettingsPlaylist.cs | 2 +- .../Online/TestSceneMultiplayerQueueList.cs | 4 +- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 46 ++++--------------- 4 files changed, 23 insertions(+), 41 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index f6c15f314e..d0cf17db1f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -248,6 +248,16 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("owner visible", () => playlist.ChildrenOfType().All(a => a.IsPresent == withOwner)); } + [Test] + public void TestWithAllButtonsEnabled() + { + createPlaylist(p => + { + p.AllowDeletion = true; + p.AllowShowingResults = true; + }); + } + private void moveToItem(int index, Vector2? offset = null) => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset)); @@ -263,7 +273,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void assertDeleteButtonVisibility(int index, bool visible) => AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible", - () => (playlist.ChildrenOfType().ElementAt(2 + index * 2).Alpha > 0) == visible); + () => (playlist.ChildrenOfType().ElementAt(2 + index * 2).RemoveButton.Alpha > 0) == visible); private void createPlaylist(Action setupPlaylist = null) { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs index 93ccd5f1e1..c61b99c2c2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs @@ -126,7 +126,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void moveToDeleteButton(int index, Vector2? offset = null) => AddStep($"move mouse to delete button {index}", () => { var item = playlist.ChildrenOfType>().ElementAt(index); - InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0), offset); + InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0).RemoveButton, offset); }); private void createPlaylist() diff --git a/osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs index 19e5624a04..95cef391f1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs @@ -154,7 +154,7 @@ namespace osu.Game.Tests.Visual.Online AddStep($"move mouse to delete button {index}", () => { item = playlist.ChildrenOfType>().ElementAt(index); - InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0)); + InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0).RemoveButton); }); AddStep("click", () => InputManager.Click(MouseButton.Left)); @@ -164,6 +164,6 @@ namespace osu.Game.Tests.Visual.Online private void assertDeleteButtonVisibility(int index, bool visible) => AddUntilStep($"delete button {index} {(visible ? "is" : "is not")} visible", - () => (playlist.ChildrenOfType().ElementAt(index).Alpha > 0) == visible); + () => (playlist.ChildrenOfType().ElementAt(index).RemoveButton.Alpha > 0) == visible); } } diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 9ad43fced5..ac5f77a72b 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -59,6 +59,8 @@ namespace osu.Game.Screens.OnlinePlay public readonly PlaylistItem Item; + public Drawable RemoveButton { get; private set; } + private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both }; private readonly IBindable valid = new Bindable(); private readonly Bindable beatmap = new Bindable(); @@ -73,7 +75,6 @@ namespace osu.Game.Screens.OnlinePlay private ModDisplay modDisplay; private FillFlowContainer buttonsFlow; private UpdateableAvatar ownerAvatar; - private Drawable removeButton; private Drawable showResultsButton; private PanelBackground panelBackground; private FillFlowContainer mainFillFlow; @@ -191,8 +192,8 @@ namespace osu.Game.Screens.OnlinePlay { allowDeletion = value; - if (removeButton != null) - removeButton.Alpha = value ? 1 : 0; + if (RemoveButton != null) + RemoveButton.Alpha = value ? 1 : 0; } } @@ -408,35 +409,23 @@ namespace osu.Game.Screens.OnlinePlay private IEnumerable createButtons() => new[] { - showResultsButton = new ShowResultsButton + showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie) { + Size = new Vector2(30, 30), Action = () => RequestResults?.Invoke(Item), Alpha = AllowShowingResults ? 1 : 0, + TooltipText = "View results" }, Item.Beatmap.Value == null ? Empty() : new PlaylistDownloadButton(Item), - removeButton = new PlaylistRemoveButton + RemoveButton = new GrayButton(FontAwesome.Solid.MinusSquare) { Size = new Vector2(30, 30), Alpha = AllowDeletion ? 1 : 0, Action = () => RequestDeletion?.Invoke(Item), + TooltipText = "Remove from playlist" }, }; - public class PlaylistRemoveButton : GrayButton - { - public PlaylistRemoveButton() - : base(FontAwesome.Solid.MinusSquare) - { - TooltipText = "Remove from playlist"; - } - - [BackgroundDependencyLoader] - private void load() - { - Icon.Scale = new Vector2(0.8f); - } - } - protected override bool OnClick(ClickEvent e) { if (AllowSelection && valid.Value) @@ -497,23 +486,6 @@ namespace osu.Game.Screens.OnlinePlay } } - private class ShowResultsButton : IconButton - { - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Icon = FontAwesome.Solid.ChartPie; - TooltipText = "View results"; - - Add(new Box - { - RelativeSizeAxes = Axes.Both, - Depth = float.MaxValue, - Colour = colours.Gray4, - }); - } - } - // For now, this is the same implementation as in PanelBackground, but supports a beatmap info rather than a working beatmap private class PanelBackground : Container // todo: should be a buffered container (https://github.com/ppy/osu-framework/issues/3222) { From 05aa9635a8cee0503e30a5200a37ccd3c80114c9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 10 Dec 2021 00:38:18 +0900 Subject: [PATCH 277/419] Privatise button again --- .../TestSceneDrawableRoomPlaylist.cs | 2 +- .../TestScenePlaylistsRoomSettingsPlaylist.cs | 2 +- .../Online/TestSceneMultiplayerQueueList.cs | 4 ++-- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 17 ++++++++++++----- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index d0cf17db1f..24d7d62aa3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -273,7 +273,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void assertDeleteButtonVisibility(int index, bool visible) => AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible", - () => (playlist.ChildrenOfType().ElementAt(2 + index * 2).RemoveButton.Alpha > 0) == visible); + () => (playlist.ChildrenOfType().ElementAt(2 + index * 2).Alpha > 0) == visible); private void createPlaylist(Action setupPlaylist = null) { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs index c61b99c2c2..93ccd5f1e1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs @@ -126,7 +126,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void moveToDeleteButton(int index, Vector2? offset = null) => AddStep($"move mouse to delete button {index}", () => { var item = playlist.ChildrenOfType>().ElementAt(index); - InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0).RemoveButton, offset); + InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0), offset); }); private void createPlaylist() diff --git a/osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs index 95cef391f1..19e5624a04 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs @@ -154,7 +154,7 @@ namespace osu.Game.Tests.Visual.Online AddStep($"move mouse to delete button {index}", () => { item = playlist.ChildrenOfType>().ElementAt(index); - InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0).RemoveButton); + InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0)); }); AddStep("click", () => InputManager.Click(MouseButton.Left)); @@ -164,6 +164,6 @@ namespace osu.Game.Tests.Visual.Online private void assertDeleteButtonVisibility(int index, bool visible) => AddUntilStep($"delete button {index} {(visible ? "is" : "is not")} visible", - () => (playlist.ChildrenOfType().ElementAt(index).RemoveButton.Alpha > 0) == visible); + () => (playlist.ChildrenOfType().ElementAt(index).Alpha > 0) == visible); } } diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index ac5f77a72b..9640d9db46 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -59,8 +59,6 @@ namespace osu.Game.Screens.OnlinePlay public readonly PlaylistItem Item; - public Drawable RemoveButton { get; private set; } - private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both }; private readonly IBindable valid = new Bindable(); private readonly Bindable beatmap = new Bindable(); @@ -76,6 +74,7 @@ namespace osu.Game.Screens.OnlinePlay private FillFlowContainer buttonsFlow; private UpdateableAvatar ownerAvatar; private Drawable showResultsButton; + private Drawable removeButton; private PanelBackground panelBackground; private FillFlowContainer mainFillFlow; @@ -192,8 +191,8 @@ namespace osu.Game.Screens.OnlinePlay { allowDeletion = value; - if (RemoveButton != null) - RemoveButton.Alpha = value ? 1 : 0; + if (removeButton != null) + removeButton.Alpha = value ? 1 : 0; } } @@ -417,7 +416,7 @@ namespace osu.Game.Screens.OnlinePlay TooltipText = "View results" }, Item.Beatmap.Value == null ? Empty() : new PlaylistDownloadButton(Item), - RemoveButton = new GrayButton(FontAwesome.Solid.MinusSquare) + removeButton = new PlaylistRemoveButton { Size = new Vector2(30, 30), Alpha = AllowDeletion ? 1 : 0, @@ -433,6 +432,14 @@ namespace osu.Game.Screens.OnlinePlay return true; } + public class PlaylistRemoveButton : GrayButton + { + public PlaylistRemoveButton() + : base(FontAwesome.Solid.MinusSquare) + { + } + } + private sealed class PlaylistDownloadButton : BeatmapDownloadButton { private readonly PlaylistItem playlistItem; From 4d1c06c0612a76d527d6458d04e1bb87486482be Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 10 Dec 2021 01:03:36 +0900 Subject: [PATCH 278/419] Add support for host enqueueing in TestMultiplayerClient --- .../Multiplayer/TestMultiplayerClient.cs | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index f151add430..1516d0e473 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -309,31 +309,36 @@ namespace osu.Game.Tests.Visual.Multiplayer Debug.Assert(APIRoom != null); Debug.Assert(currentItem != null); + bool isNewAddition = item.ID == 0; + if (Room.Settings.QueueMode == QueueMode.HostOnly && Room.Host?.UserID != LocalUser?.UserID) throw new InvalidOperationException("Local user is not the room host."); item.OwnerID = userId; - switch (Room.Settings.QueueMode) + if (isNewAddition) { - case QueueMode.HostOnly: - // In host-only mode, the current item is re-used. - item.ID = currentItem.ID; - item.PlaylistOrder = currentItem.PlaylistOrder; + await addItem(item).ConfigureAwait(false); + await updateCurrentItem(Room).ConfigureAwait(false); + } + else + { + var existingItem = serverSidePlaylist.SingleOrDefault(i => i.ID == item.ID); - serverSidePlaylist[currentIndex] = item; - await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); + if (existingItem == null) + throw new InvalidOperationException("Attempted to change an item that doesn't exist."); - // Note: Unlike the server, this is the easiest way to update the current item at this point. - await updateCurrentItem(Room, false).ConfigureAwait(false); - break; + if (existingItem.OwnerID != userId && Room.Host?.UserID != LocalUser?.UserID) + throw new InvalidOperationException("Attempted to change an item which is not owned by the user."); - default: - await addItem(item).ConfigureAwait(false); + if (existingItem.Expired) + throw new InvalidOperationException("Attempted to change an item which has already been played."); - // The current item can change as a result of an item being added. For example, if all items earlier in the queue were expired. - await updateCurrentItem(Room).ConfigureAwait(false); - break; + // Ensure the playlist order doesn't change. + item.PlaylistOrder = existingItem.PlaylistOrder; + + serverSidePlaylist[serverSidePlaylist.IndexOf(existingItem)] = item; + await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); } } From 048a4951156593ae4c2a45d019fe15210228ca2c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 10 Dec 2021 01:08:54 +0900 Subject: [PATCH 279/419] Add edit button to DrawableRoomPlaylistItem --- .../TestSceneDrawableRoomPlaylist.cs | 1 + .../OnlinePlay/DrawableRoomPlaylist.cs | 27 ++++++++++++- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 38 +++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 24d7d62aa3..f9784384fd 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -255,6 +255,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { p.AllowDeletion = true; p.AllowShowingResults = true; + p.AllowEditing = true; }); } diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 5e180afcf9..57bb4253cb 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -33,6 +33,11 @@ namespace osu.Game.Screens.OnlinePlay /// public Action RequestResults; + /// + /// Invoked when an item requests to be edited. + /// + public Action RequestEdit; + private bool allowReordering; /// @@ -104,6 +109,24 @@ namespace osu.Game.Screens.OnlinePlay } } + private bool allowEditing; + + /// + /// Whether to allow items to be edited. + /// If true, requests to edit items may be satisfied via . + /// + public bool AllowEditing + { + get => allowEditing; + set + { + allowEditing = value; + + foreach (var item in ListContainer.OfType()) + item.AllowEditing = value; + } + } + private bool showItemOwners; /// @@ -135,12 +158,14 @@ namespace osu.Game.Screens.OnlinePlay { d.SelectedItem.BindTarget = SelectedItem; d.RequestDeletion = i => RequestDeletion?.Invoke(i); + d.RequestResults = i => RequestResults?.Invoke(i); + d.RequestEdit = i => RequestEdit?.Invoke(i); d.AllowReordering = AllowReordering; d.AllowDeletion = AllowDeletion; d.AllowSelection = AllowSelection; d.AllowShowingResults = AllowShowingResults; + d.AllowEditing = AllowEditing; d.ShowItemOwner = ShowItemOwners; - d.RequestResults = i => RequestResults?.Invoke(i); }); protected virtual DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new DrawableRoomPlaylistItem(item); diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 9640d9db46..8042f7d772 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -51,6 +51,11 @@ namespace osu.Game.Screens.OnlinePlay /// public Action RequestResults; + /// + /// Invoked when this item requests to be edited. + /// + public Action RequestEdit; + /// /// The currently-selected item, used to show a border around this item. /// May be updated by this item if is true. @@ -74,6 +79,7 @@ namespace osu.Game.Screens.OnlinePlay private FillFlowContainer buttonsFlow; private UpdateableAvatar ownerAvatar; private Drawable showResultsButton; + private Drawable editButton; private Drawable removeButton; private PanelBackground panelBackground; private FillFlowContainer mainFillFlow; @@ -213,6 +219,23 @@ namespace osu.Game.Screens.OnlinePlay } } + private bool allowEditing; + + /// + /// Whether this item can be edited. + /// + public bool AllowEditing + { + get => allowEditing; + set + { + allowEditing = value; + + if (editButton != null) + editButton.Alpha = value ? 1 : 0; + } + } + private bool showItemOwner; /// @@ -416,6 +439,13 @@ namespace osu.Game.Screens.OnlinePlay TooltipText = "View results" }, Item.Beatmap.Value == null ? Empty() : new PlaylistDownloadButton(Item), + editButton = new PlaylistEditButton + { + Size = new Vector2(30, 30), + Alpha = AllowEditing ? 1 : 0, + Action = () => RequestEdit?.Invoke(Item), + TooltipText = "Edit" + }, removeButton = new PlaylistRemoveButton { Size = new Vector2(30, 30), @@ -432,6 +462,14 @@ namespace osu.Game.Screens.OnlinePlay return true; } + public class PlaylistEditButton : GrayButton + { + public PlaylistEditButton() + : base(FontAwesome.Solid.Edit) + { + } + } + public class PlaylistRemoveButton : GrayButton { public PlaylistRemoveButton() From 671582a9255706334c67f75a0cece57a454be14a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 10 Dec 2021 01:15:15 +0900 Subject: [PATCH 280/419] Allow host to enqeue items and items to be edited --- .../TestSceneAllPlayersQueueMode.cs | 4 +- .../Multiplayer/TestSceneHostOnlyQueueMode.cs | 24 ++++++++++- .../Multiplayer/TestSceneMultiplayer.cs | 3 +- .../TestSceneMultiplayerMatchSongSelect.cs | 2 +- .../Match/Playlist/MultiplayerPlaylist.cs | 9 +++- .../Match/Playlist/MultiplayerQueueList.cs | 6 ++- .../Multiplayer/MultiplayerMatchSongSelect.cs | 8 +++- .../Multiplayer/MultiplayerMatchSubScreen.cs | 42 +++++++------------ .../OnlinePlay/OnlinePlaySongSelect.cs | 12 +++--- 9 files changed, 67 insertions(+), 43 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs index ccfae1deef..8373979308 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs @@ -87,9 +87,9 @@ namespace osu.Game.Tests.Visual.Multiplayer private void addItem(Func beatmap) { - AddStep("click edit button", () => + AddStep("click add button", () => { - InputManager.MoveMouseTo(this.ChildrenOfType().Single().AddOrEditPlaylistButton); + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs index 1de7289446..ccac3de304 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Online.Multiplayer; +using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Multiplayer; using osuTK.Input; @@ -74,11 +75,19 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("api room updated", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); } + [Test] + public void TestAddItemsAsHost() + { + addItem(() => OtherBeatmap); + + AddAssert("playlist contains two items", () => Client.APIRoom?.Playlist.Count == 2); + } + private void selectNewItem(Func beatmap) { AddStep("click edit button", () => { - InputManager.MoveMouseTo(this.ChildrenOfType().Single().AddOrEditPlaylistButton); + InputManager.MoveMouseTo(this.ChildrenOfType().First()); InputManager.Click(MouseButton.Left); }); @@ -90,5 +99,18 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen); AddUntilStep("selected item is new beatmap", () => Client.CurrentMatchPlayingItem.Value?.Beatmap.Value?.OnlineID == otherBeatmap.OnlineID); } + + private void addItem(Func beatmap) + { + AddStep("click add button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded); + AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(beatmap())); + AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen); + } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 2411f39ae3..5eb0abc830 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -416,8 +416,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("Enter song select", () => { var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerScreenStack.CurrentScreen).CurrentSubScreen; - - ((MultiplayerMatchSubScreen)currentSubScreen).SelectBeatmap(); + ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.CurrentMatchPlayingItem.Value); }); AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index a5229702a8..d671673d3c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -179,7 +179,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public new BeatmapCarousel Carousel => base.Carousel; public TestMultiplayerMatchSongSelect(Room room, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null) - : base(room, beatmap, ruleset) + : base(room, null, beatmap, ruleset) { } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index c3245b550f..4971489769 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -19,6 +20,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist { public readonly Bindable DisplayMode = new Bindable(); + /// + /// Invoked when an item requests to be edited. + /// + public Action RequestEdit; + private MultiplayerQueueList queueList; private MultiplayerHistoryList historyList; private bool firstPopulation = true; @@ -46,7 +52,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist queueList = new MultiplayerQueueList { RelativeSizeAxes = Axes.Both, - SelectedItem = { BindTarget = SelectedItem } + SelectedItem = { BindTarget = SelectedItem }, + RequestEdit = item => RequestEdit?.Invoke(item) }, historyList = new MultiplayerHistoryList { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index 8832c9bd60..3e0f663d42 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -78,8 +78,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist private void updateDeleteButtonVisibility() { - AllowDeletion = (Item.OwnerID == api.LocalUser.Value.OnlineID || multiplayerClient.IsHost) - && SelectedItem.Value != Item; + bool isItemOwner = Item.OwnerID == api.LocalUser.Value.OnlineID || multiplayerClient.IsHost; + + AllowDeletion = isItemOwner && SelectedItem.Value != Item; + AllowEditing = isItemOwner; } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 44efef53f5..80a0289ba9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -24,17 +24,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } + private readonly PlaylistItem itemToEdit; + private LoadingLayer loadingLayer; /// /// Construct a new instance of multiplayer song select. /// /// The room. + /// The item to be edited. May be null, in which case a new item will be added to the playlist. /// An optional initial beatmap selection to perform. /// An optional initial ruleset selection to perform. - public MultiplayerMatchSongSelect(Room room, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null) + public MultiplayerMatchSongSelect(Room room, PlaylistItem itemToEdit = null, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null) : base(room) { + this.itemToEdit = itemToEdit; + if (beatmap != null || ruleset != null) { Schedule(() => @@ -61,6 +66,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.AddPlaylistItem(new MultiplayerPlaylistItem { + ID = itemToEdit?.ID ?? 0, BeatmapID = item.BeatmapID, BeatmapChecksum = item.Beatmap.Value.MD5Hash, RulesetID = item.RulesetID, diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 3a25bd7b06..96a9804067 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -14,7 +14,6 @@ using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; -using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -44,8 +43,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public override string ShortTitle => "room"; - public OsuButton AddOrEditPlaylistButton { get; private set; } - [Resolved] private MultiplayerClient client { get; set; } @@ -57,6 +54,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [CanBeNull] private IDisposable readyClickOperation; + private AddItemButton addItemButton; + public MultiplayerMatchSubScreen(Room room) : base(room) { @@ -134,12 +133,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new Drawable[] { new OverlinedHeader("Beatmap") }, new Drawable[] { - AddOrEditPlaylistButton = new PurpleTriangleButton + addItemButton = new AddItemButton { RelativeSizeAxes = Axes.X, Height = 40, - Action = SelectBeatmap, - Alpha = 0 + Text = "Add item", + Action = () => OpenSongSelection(null) }, }, null, @@ -147,7 +146,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { new MultiplayerPlaylist { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + RequestEdit = OpenSongSelection } }, new[] @@ -220,12 +220,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } }; - internal void SelectBeatmap() + internal void OpenSongSelection(PlaylistItem itemToEdit) { if (!this.IsCurrentScreen()) return; - this.Push(new MultiplayerMatchSongSelect(Room)); + this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit)); } protected override Drawable CreateFooter() => new MultiplayerMatchFooter @@ -385,23 +385,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return; } - switch (client.Room.Settings.QueueMode) - { - case QueueMode.HostOnly: - AddOrEditPlaylistButton.Text = "Edit beatmap"; - AddOrEditPlaylistButton.Alpha = client.IsHost ? 1 : 0; - break; - - case QueueMode.AllPlayers: - case QueueMode.AllPlayersRoundRobin: - AddOrEditPlaylistButton.Text = "Add beatmap"; - AddOrEditPlaylistButton.Alpha = 1; - break; - - default: - AddOrEditPlaylistButton.Alpha = 0; - break; - } + addItemButton.Alpha = client.IsHost || Room.QueueMode.Value != QueueMode.HostOnly ? 1 : 0; Scheduler.AddOnce(UpdateMods); } @@ -466,7 +450,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return; } - this.Push(new MultiplayerMatchSongSelect(Room, beatmap, ruleset)); + this.Push(new MultiplayerMatchSongSelect(Room, SelectedItem.Value, beatmap, ruleset)); } protected override void Dispose(bool isDisposing) @@ -481,5 +465,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer modSettingChangeTracker?.Dispose(); } + + public class AddItemButton : PurpleTriangleButton + { + } } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 4bc0b55433..63957caee3 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -33,14 +33,14 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room), nameof(Room.Playlist))] protected BindableList Playlist { get; private set; } + [CanBeNull] + [Resolved(CanBeNull = true)] + protected IBindable SelectedItem { get; private set; } + protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); - [CanBeNull] - [Resolved(CanBeNull = true)] - private IBindable selectedItem { get; set; } - private readonly FreeModSelectOverlay freeModSelectOverlay; private readonly Room room; @@ -80,8 +80,8 @@ namespace osu.Game.Screens.OnlinePlay // At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods. // Similarly, freeMods is currently empty but should only contain the allowed mods. - Mods.Value = selectedItem?.Value?.RequiredMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty(); - FreeMods.Value = selectedItem?.Value?.AllowedMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty(); + Mods.Value = SelectedItem?.Value?.RequiredMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty(); + FreeMods.Value = SelectedItem?.Value?.AllowedMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty(); Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); From a445dcd2c60a0f5456884f8346de75b37717ea2e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 10 Dec 2021 02:09:31 +0900 Subject: [PATCH 281/419] Fix incorrect test namespace --- .../{Online => Multiplayer}/TestSceneMultiplayerQueueList.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename osu.Game.Tests/Visual/{Online => Multiplayer}/TestSceneMultiplayerQueueList.cs (98%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs similarity index 98% rename from osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 19e5624a04..a2b2da0aec 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -20,11 +20,10 @@ using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; using osu.Game.Tests.Resources; -using osu.Game.Tests.Visual.Multiplayer; using osuTK; using osuTK.Input; -namespace osu.Game.Tests.Visual.Online +namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayerQueueList : MultiplayerTestScene { From 612f47bb9ff7eaf565def5970fc24daf80f7007d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Dec 2021 13:45:29 +0900 Subject: [PATCH 282/419] Add the ability to create playlists of 2 weeks ~ 3 months in duration --- .../Playlists/PlaylistsRoomSettingsOverlay.cs | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index 8f31422add..6c8ab52d22 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -5,7 +5,9 @@ using System; using System.Collections.Specialized; using System.Linq; using Humanizer; +using Humanizer.Localisation; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -14,6 +16,8 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Match.Components; @@ -69,6 +73,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [Resolved(CanBeNull = true)] private IRoomManager manager { get; set; } + [Resolved] + private IAPIProvider api { get; set; } + private readonly Room room; public MatchSettings(Room room) @@ -134,19 +141,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Child = DurationField = new DurationDropdown { RelativeSizeAxes = Axes.X, - Items = new[] - { - TimeSpan.FromMinutes(30), - TimeSpan.FromHours(1), - TimeSpan.FromHours(2), - TimeSpan.FromHours(4), - TimeSpan.FromHours(8), - TimeSpan.FromHours(12), - //TimeSpan.FromHours(16), - TimeSpan.FromHours(24), - TimeSpan.FromDays(3), - TimeSpan.FromDays(7) - } } }, new Section("Allowed attempts (across all playlist items)") @@ -303,10 +297,40 @@ namespace osu.Game.Screens.OnlinePlay.Playlists MaxAttempts.BindValueChanged(count => MaxAttemptsField.Text = count.NewValue?.ToString(), true); Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true); + api.LocalUser.BindValueChanged(populateDurations, true); + playlist.Items.BindTo(Playlist); Playlist.BindCollectionChanged(onPlaylistChanged, true); } + private void populateDurations(ValueChangedEvent user) + { + DurationField.Items = new[] + { + TimeSpan.FromMinutes(30), + TimeSpan.FromHours(1), + TimeSpan.FromHours(2), + TimeSpan.FromHours(4), + TimeSpan.FromHours(8), + TimeSpan.FromHours(12), + TimeSpan.FromHours(24), + TimeSpan.FromDays(3), + TimeSpan.FromDays(7), + TimeSpan.FromDays(14), + }; + + // TODO: show these in the interface at all times. + if (user.NewValue.IsSupporter) + { + // roughly correct (see https://github.com/Humanizr/Humanizer/blob/18167e56c082449cc4fe805b8429e3127a7b7f93/readme.md?plain=1#L427) + // if we want this to be more accurate we might consider sending an actual end time, not a time span. probably not required though. + const int days_in_month = 31; + + DurationField.AddDropdownItem(TimeSpan.FromDays(days_in_month)); + DurationField.AddDropdownItem(TimeSpan.FromDays(days_in_month * 3)); + } + } + protected override void Update() { base.Update(); @@ -405,7 +429,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Menu.MaxHeight = 100; } - protected override LocalisableString GenerateItemText(TimeSpan item) => item.Humanize(); + protected override LocalisableString GenerateItemText(TimeSpan item) => item.Humanize(maxUnit: TimeUnit.Month); } } } From 9ac8e6c81cfa391b757168f9621f5766a2ea4078 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Dec 2021 13:53:48 +0900 Subject: [PATCH 283/419] Add missing null check before attempting to populate bpm info --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 31 +++++++++++---------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index d292b7114f..6791565828 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -163,7 +163,7 @@ namespace osu.Game.Screens.Select private FillFlowContainer infoLabelContainer; private Container bpmLabelContainer; - private readonly WorkingBeatmap beatmap; + private readonly WorkingBeatmap working; private readonly RulesetInfo ruleset; [Resolved] @@ -171,10 +171,10 @@ namespace osu.Game.Screens.Select private ModSettingChangeTracker settingChangeTracker; - public WedgeInfoText(WorkingBeatmap beatmap, RulesetInfo userRuleset) + public WedgeInfoText(WorkingBeatmap working, RulesetInfo userRuleset) { - this.beatmap = beatmap; - ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset; + this.working = working; + ruleset = userRuleset ?? working.BeatmapInfo.Ruleset; } private CancellationTokenSource cancellationSource; @@ -183,8 +183,8 @@ namespace osu.Game.Screens.Select [BackgroundDependencyLoader] private void load(OsuColour colours, LocalisationManager localisation, BeatmapDifficultyCache difficultyCache) { - var beatmapInfo = beatmap.BeatmapInfo; - var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); + var beatmapInfo = working.BeatmapInfo; + var metadata = beatmapInfo.Metadata ?? working.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); RelativeSizeAxes = Axes.Both; @@ -353,7 +353,7 @@ namespace osu.Game.Screens.Select private void addInfoLabels() { - if (beatmap.Beatmap?.HitObjects?.Any() != true) + if (working.Beatmap?.HitObjects?.Any() != true) return; infoLabelContainer.Children = new Drawable[] @@ -362,7 +362,7 @@ namespace osu.Game.Screens.Select { Name = "Length", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length), - Content = beatmap.BeatmapInfo.Length.ToFormattedDuration().ToString(), + Content = working.BeatmapInfo.Length.ToFormattedDuration().ToString(), }), bpmLabelContainer = new Container { @@ -386,12 +386,12 @@ namespace osu.Game.Screens.Select try { // Try to get the beatmap with the user's ruleset - playableBeatmap = beatmap.GetPlayableBeatmap(ruleset, Array.Empty()); + playableBeatmap = working.GetPlayableBeatmap(ruleset, Array.Empty()); } catch (BeatmapInvalidForRulesetException) { // Can't be converted to the user's ruleset, so use the beatmap's own ruleset - playableBeatmap = beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset, Array.Empty()); + playableBeatmap = working.GetPlayableBeatmap(working.BeatmapInfo.Ruleset, Array.Empty()); } return playableBeatmap.GetStatistics().Select(s => new InfoLabel(s)).ToArray(); @@ -406,8 +406,9 @@ namespace osu.Game.Screens.Select private void refreshBPMLabel() { - var b = beatmap.Beatmap; - if (b == null) + var beatmap = working.Beatmap; + + if (beatmap == null || bpmLabelContainer == null) return; // this doesn't consider mods which apply variable rates, yet. @@ -415,9 +416,9 @@ namespace osu.Game.Screens.Select foreach (var mod in mods.Value.OfType()) rate = mod.ApplyToRate(0, rate); - double bpmMax = b.ControlPointInfo.BPMMaximum * rate; - double bpmMin = b.ControlPointInfo.BPMMinimum * rate; - double mostCommonBPM = 60000 / b.GetMostCommonBeatLength() * rate; + double bpmMax = beatmap.ControlPointInfo.BPMMaximum * rate; + double bpmMin = beatmap.ControlPointInfo.BPMMinimum * rate; + double mostCommonBPM = 60000 / beatmap.GetMostCommonBeatLength() * rate; string labelText = Precision.AlmostEquals(bpmMin, bpmMax) ? $"{bpmMin:0}" From 88670c3b014ce53d805dc51fd5426ef8540824bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Dec 2021 14:14:22 +0900 Subject: [PATCH 284/419] Document `OpenSongSelection` and mark null param --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 96a9804067..946c749db3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -138,7 +138,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer RelativeSizeAxes = Axes.X, Height = 40, Text = "Add item", - Action = () => OpenSongSelection(null) + Action = () => OpenSongSelection() }, }, null, @@ -220,7 +220,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } }; - internal void OpenSongSelection(PlaylistItem itemToEdit) + /// + /// Opens the song selection screen to add or edit an item. + /// + /// An optional playlist item to edit. If null, a new item will be added instead. + internal void OpenSongSelection([CanBeNull] PlaylistItem itemToEdit = null) { if (!this.IsCurrentScreen()) return; From de0f37b08d60477048a9015d1dcd8bb96e019996 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 10 Dec 2021 14:44:35 +0900 Subject: [PATCH 285/419] Separate editing and adding playlist items --- .../Multiplayer/IMultiplayerRoomServer.cs | 6 ++ .../Online/Multiplayer/MultiplayerClient.cs | 2 + .../Multiplayer/OnlineMultiplayerClient.cs | 8 +++ .../Multiplayer/MultiplayerMatchSongSelect.cs | 9 ++- .../Multiplayer/TestMultiplayerClient.cs | 60 ++++++++++--------- 5 files changed, 54 insertions(+), 31 deletions(-) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 65467e6ba9..73fda78d00 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -83,6 +83,12 @@ namespace osu.Game.Online.Multiplayer /// The item to add. Task AddPlaylistItem(MultiplayerPlaylistItem item); + /// + /// Edits an existing playlist item with new values. + /// + /// The item to edit, containing new properties. Must have an ID. + Task EditPlaylistItem(MultiplayerPlaylistItem item); + /// /// Removes an item from the playlist. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 34dc7ea5ea..55b4def908 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -335,6 +335,8 @@ namespace osu.Game.Online.Multiplayer public abstract Task AddPlaylistItem(MultiplayerPlaylistItem item); + public abstract Task EditPlaylistItem(MultiplayerPlaylistItem item); + public abstract Task RemovePlaylistItem(long playlistItemId); Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state) diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 7314603603..d268d2bf69 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -162,6 +162,14 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.AddPlaylistItem), item); } + public override Task EditPlaylistItem(MultiplayerPlaylistItem item) + { + if (!IsConnected.Value) + return Task.CompletedTask; + + return connection.InvokeAsync(nameof(IMultiplayerServer.EditPlaylistItem), item); + } + public override Task RemovePlaylistItem(long playlistItemId) { if (!IsConnected.Value) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 80a0289ba9..8d3686dd6d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; using osu.Framework.Allocation; using osu.Framework.Logging; @@ -64,7 +65,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { loadingLayer.Show(); - client.AddPlaylistItem(new MultiplayerPlaylistItem + var multiplayerItem = new MultiplayerPlaylistItem { ID = itemToEdit?.ID ?? 0, BeatmapID = item.BeatmapID, @@ -72,7 +73,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer RulesetID = item.RulesetID, RequiredMods = item.RequiredMods.Select(m => new APIMod(m)).ToArray(), AllowedMods = item.AllowedMods.Select(m => new APIMod(m)).ToArray() - }).ContinueWith(t => + }; + + Task task = itemToEdit != null ? client.EditPlaylistItem(multiplayerItem) : client.AddPlaylistItem(multiplayerItem); + + task.ContinueWith(t => { Schedule(() => { diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 1516d0e473..d22f0415e6 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -309,49 +309,51 @@ namespace osu.Game.Tests.Visual.Multiplayer Debug.Assert(APIRoom != null); Debug.Assert(currentItem != null); - bool isNewAddition = item.ID == 0; - if (Room.Settings.QueueMode == QueueMode.HostOnly && Room.Host?.UserID != LocalUser?.UserID) throw new InvalidOperationException("Local user is not the room host."); item.OwnerID = userId; - if (isNewAddition) - { - await addItem(item).ConfigureAwait(false); - await updateCurrentItem(Room).ConfigureAwait(false); - } - else - { - var existingItem = serverSidePlaylist.SingleOrDefault(i => i.ID == item.ID); - - if (existingItem == null) - throw new InvalidOperationException("Attempted to change an item that doesn't exist."); - - if (existingItem.OwnerID != userId && Room.Host?.UserID != LocalUser?.UserID) - throw new InvalidOperationException("Attempted to change an item which is not owned by the user."); - - if (existingItem.Expired) - throw new InvalidOperationException("Attempted to change an item which has already been played."); - - // Ensure the playlist order doesn't change. - item.PlaylistOrder = existingItem.PlaylistOrder; - - serverSidePlaylist[serverSidePlaylist.IndexOf(existingItem)] = item; - await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); - } + await addItem(item).ConfigureAwait(false); + await updateCurrentItem(Room).ConfigureAwait(false); } public override Task AddPlaylistItem(MultiplayerPlaylistItem item) => AddUserPlaylistItem(api.LocalUser.Value.OnlineID, item); + public async Task EditUserPlaylistItem(int userId, MultiplayerPlaylistItem item) + { + Debug.Assert(Room != null); + Debug.Assert(APIRoom != null); + Debug.Assert(currentItem != null); + + item.OwnerID = userId; + + var existingItem = serverSidePlaylist.SingleOrDefault(i => i.ID == item.ID); + + if (existingItem == null) + throw new InvalidOperationException("Attempted to change an item that doesn't exist."); + + if (existingItem.OwnerID != userId && Room.Host?.UserID != LocalUser?.UserID) + throw new InvalidOperationException("Attempted to change an item which is not owned by the user."); + + if (existingItem.Expired) + throw new InvalidOperationException("Attempted to change an item which has already been played."); + + // Ensure the playlist order doesn't change. + item.PlaylistOrder = existingItem.PlaylistOrder; + + serverSidePlaylist[serverSidePlaylist.IndexOf(existingItem)] = item; + + await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); + } + + public override Task EditPlaylistItem(MultiplayerPlaylistItem item) => EditUserPlaylistItem(api.LocalUser.Value.OnlineID, item); + public async Task RemoveUserPlaylistItem(int userId, long playlistItemId) { Debug.Assert(Room != null); Debug.Assert(APIRoom != null); - if (Room.Settings.QueueMode == QueueMode.HostOnly) - throw new InvalidOperationException("Items cannot be removed in host-only mode."); - var item = serverSidePlaylist.Find(i => i.ID == playlistItemId); if (item == null) From 261847bbec333933dd4f848976f78ba3b58fdd35 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Dec 2021 15:29:31 +0900 Subject: [PATCH 286/419] Avoid touching `ScoreInfo.User` directly --- osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs index b7b7407428..2f9652d354 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs @@ -159,8 +159,8 @@ namespace osu.Game.Tests.Visual.Ranking var firstScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); var secondScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); - firstScore.User.Username = "A"; - secondScore.User.Username = "B"; + firstScore.UserString = "A"; + secondScore.UserString = "B"; createListStep(() => new ScorePanelList()); From bf1418bafc6a3d61ad03ccaa6c00f4f9e125b4d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Dec 2021 15:28:41 +0900 Subject: [PATCH 287/419] Use `OnlineID` instead of legacy IDs for equality and lookups --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 2 +- .../Visual/Playlists/TestScenePlaylistsResultsScreen.cs | 9 +++++---- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 4 ++-- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 2 +- osu.Game/OsuGame.cs | 4 ++-- osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs | 2 +- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 2 +- osu.Game/Scoring/ScoreInfo.cs | 4 ++-- osu.Game/Scoring/ScoreManager.cs | 2 +- osu.Game/Scoring/ScoreModelManager.cs | 2 +- .../OnlinePlay/Playlists/PlaylistsResultsScreen.cs | 4 ++-- osu.Game/Screens/Play/SpectatorPlayer.cs | 2 +- osu.Game/Screens/Ranking/ScorePanelList.cs | 2 +- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 2 +- 14 files changed, 22 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 0dee0f89ea..4a01117031 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Scores.IO Assert.AreEqual(toImport.Combo, imported.Combo); Assert.AreEqual(toImport.User.Username, imported.User.Username); Assert.AreEqual(toImport.Date, imported.Date); - Assert.AreEqual(toImport.OnlineScoreID, imported.OnlineScoreID); + Assert.AreEqual(toImport.OnlineID, imported.OnlineID); } finally { diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 4284bc6358..38dd507e00 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -30,6 +30,7 @@ namespace osu.Game.Tests.Visual.Playlists private const int scores_per_result = 10; private TestResultsScreen resultsScreen; + private int currentScoreId; private bool requestComplete; private int totalCount; @@ -37,7 +38,7 @@ namespace osu.Game.Tests.Visual.Playlists [SetUp] public void Setup() => Schedule(() => { - currentScoreId = 0; + currentScoreId = 1; requestComplete = false; totalCount = 0; bindHandler(); @@ -56,7 +57,7 @@ namespace osu.Game.Tests.Visual.Playlists createResults(() => userScore); - AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded); + AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineScoreID).State == PanelState.Expanded); } [Test] @@ -81,7 +82,7 @@ namespace osu.Game.Tests.Visual.Playlists createResults(() => userScore); AddAssert("more than 1 panel displayed", () => this.ChildrenOfType().Count() > 1); - AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded); + AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineScoreID).State == PanelState.Expanded); } [Test] @@ -230,7 +231,7 @@ namespace osu.Game.Tests.Visual.Playlists { var multiplayerUserScore = new MultiplayerScore { - ID = (int)(userScore.OnlineScoreID ?? currentScoreId++), + ID = (int)(userScore.OnlineID > 0 ? userScore.OnlineID : currentScoreId++), Accuracy = userScore.Accuracy, EndedAt = userScore.Date, Passed = userScore.Passed, diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 9f0f4a6b8b..d22eff9e75 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -163,7 +163,7 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddUntilStep("wait for fetch", () => leaderboard.Scores != null); - AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID)); + AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != scoreBeingDeleted.OnlineID)); } [Test] @@ -171,7 +171,7 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep("delete top score", () => scoreManager.Delete(importedScores[0])); AddUntilStep("wait for fetch", () => leaderboard.Scores != null); - AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != importedScores[0].OnlineScoreID)); + AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != importedScores[0].OnlineID)); } } } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 644c2e2a99..14eec8b388 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -111,7 +111,7 @@ namespace osu.Game.Online.Leaderboards background = new Box { RelativeSizeAxes = Axes.Both, - Colour = user.Id == api.LocalUser.Value.Id && allowHighlight ? colour.Green : Color4.Black, + Colour = user.OnlineID == api.LocalUser.Value.Id && allowHighlight ? colour.Green : Color4.Black, Alpha = background_alpha, }, }, diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 047c3b4225..565ee7e71d 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -487,8 +487,8 @@ namespace osu.Game // to ensure all the required data for presenting a replay are present. ScoreInfo databasedScoreInfo = null; - if (score.OnlineScoreID != null) - databasedScoreInfo = ScoreManager.Query(s => s.OnlineScoreID == score.OnlineScoreID); + if (score.OnlineID > 0) + databasedScoreInfo = ScoreManager.Query(s => s.OnlineID == score.OnlineID); databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == score.Hash); diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index 2fcdc9402d..695661d5c9 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -94,7 +94,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores topScoresContainer.Add(new DrawableTopScore(topScore)); - if (userScoreInfo != null && userScoreInfo.OnlineScoreID != topScore.OnlineScoreID) + if (userScoreInfo != null && userScoreInfo.OnlineID != topScore.OnlineID) topScoresContainer.Add(new DrawableTopScore(userScoreInfo, userScore.Position)); }), TaskContinuationOptions.OnlyOnRanToCompletion); }); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index f943422389..b46c1a27ed 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -84,7 +84,7 @@ namespace osu.Game.Scoring.Legacy else if (version >= 20121008) scoreInfo.OnlineScoreID = sr.ReadInt32(); - if (scoreInfo.OnlineScoreID <= 0) + if (scoreInfo.OnlineID <= 0) scoreInfo.OnlineScoreID = null; if (compressedReplay?.Length > 0) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 5dc88d7644..ba20ec030a 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -237,8 +237,8 @@ namespace osu.Game.Scoring if (ID != 0 && other.ID != 0) return ID == other.ID; - if (OnlineScoreID.HasValue && other.OnlineScoreID.HasValue) - return OnlineScoreID == other.OnlineScoreID; + if (OnlineID > 0) + return OnlineID == other.OnlineID; if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash)) return Hash == other.Hash; diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index e9cd44ae83..6de6b57066 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -71,7 +71,7 @@ namespace osu.Game.Scoring return scores.Select((score, index) => (score, totalScore: totalScores[index])) .OrderByDescending(g => g.totalScore) - .ThenBy(g => g.score.OnlineScoreID) + .ThenBy(g => g.score.OnlineID) .Select(g => g.score) .ToArray(); } diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs index 2cbd3aded7..44f0fe4fdf 100644 --- a/osu.Game/Scoring/ScoreModelManager.cs +++ b/osu.Game/Scoring/ScoreModelManager.cs @@ -66,6 +66,6 @@ namespace osu.Game.Scoring protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable items) => base.CheckLocalAvailability(model, items) - || (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID)); + || (model.OnlineID > 0 && items.Any(i => i.OnlineID == model.OnlineID)); } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs index aed3635cbc..67727ef784 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs @@ -186,12 +186,12 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Schedule(() => { // Prefer selecting the local user's score, or otherwise default to the first visible score. - SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.Id == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault(); + SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.OnlineID == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault(); }); } // Invoke callback to add the scores. Exclude the user's current score which was added previously. - callback.Invoke(scoreInfos.Where(s => s.OnlineScoreID != Score?.OnlineScoreID)); + callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID)); hideLoadingSpinners(pivot); })); diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index f6a89e7fa9..d42643c416 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.Play private void userSentFrames(int userId, FrameDataBundle bundle) { - if (userId != score.ScoreInfo.User.Id) + if (userId != score.ScoreInfo.User.OnlineID) return; if (!LoadedBeatmapSuccessfully) diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 22be91b974..f3de48dcf0 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -341,7 +341,7 @@ namespace osu.Game.Screens.Ranking private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType() .OrderByDescending(GetLayoutPosition) - .ThenBy(s => s.Panel.Score.OnlineScoreID); + .ThenBy(s => s.Panel.Score.OnlineID); } private class Scroll : OsuScrollContainer diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 929bda6508..afebc728b4 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Ranking return null; getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); - getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineID != Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets, Beatmap.Value.BeatmapInfo))); + getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineID != Score.OnlineID).Select(s => s.CreateScoreInfo(rulesets, Beatmap.Value.BeatmapInfo))); return getScoreRequest; } From 7ac63485ef03d40ae987ae27eb7d25832ed3d97d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Dec 2021 15:31:35 +0900 Subject: [PATCH 288/419] Add setter for `ScoreInfo.OnlineID` --- osu.Game/Scoring/ScoreInfo.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index ba20ec030a..d6b7b2712b 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -248,7 +248,12 @@ namespace osu.Game.Scoring #region Implementation of IHasOnlineID - public long OnlineID => OnlineScoreID ?? -1; + [NotMapped] + public long OnlineID + { + get => OnlineScoreID ?? -1; + set => OnlineScoreID = value; + } #endregion From dbb08f7d463aca7cb0d6e8206766f4aaf53821c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Dec 2021 15:37:12 +0900 Subject: [PATCH 289/419] Use `OnlineID` for set operations --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 2 +- osu.Game.Tests/Database/BeatmapImporterTests.cs | 2 +- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 6 +++--- .../Visual/Multiplayer/TestSceneMultiplayerResults.cs | 2 +- .../Multiplayer/TestSceneMultiplayerTeamResults.cs | 2 +- .../Visual/Navigation/TestScenePresentScore.cs | 2 +- .../Playlists/TestScenePlaylistsResultsScreen.cs | 10 +++++----- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 2 +- osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs | 2 +- osu.Game/Online/Rooms/MultiplayerScore.cs | 2 +- osu.Game/Online/ScoreDownloadTracker.cs | 2 +- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 7 ++----- osu.Game/Screens/Play/Player.cs | 6 +++--- osu.Game/Screens/Play/SubmittingPlayer.cs | 2 +- 14 files changed, 23 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 6e2b9d20a8..6d0d5702e9 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -1022,7 +1022,7 @@ namespace osu.Game.Tests.Beatmaps.IO { return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo { - OnlineScoreID = 2, + OnlineID = 2, BeatmapInfo = beatmapInfo, BeatmapInfoID = beatmapInfo.ID }, new ImportScoreTest.TestArchiveReader()); diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index a6edd6cb5f..e47e24021f 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -809,7 +809,7 @@ namespace osu.Game.Tests.Database // TODO: reimplement when we have score support in realm. // return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo // { - // OnlineScoreID = 2, + // OnlineID = 2, // Beatmap = beatmap, // BeatmapInfoID = beatmap.ID // }, new ImportScoreTest.TestArchiveReader()); diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 4a01117031..bbc92b7817 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Scores.IO Combo = 250, User = new APIUser { Username = "Test user" }, Date = DateTimeOffset.Now, - OnlineScoreID = 12345, + OnlineID = 12345, }; var imported = await LoadScoreIntoOsu(osu, toImport); @@ -163,12 +163,12 @@ namespace osu.Game.Tests.Scores.IO { var osu = LoadOsuIntoHost(host, true); - await LoadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader()); + await LoadScoreIntoOsu(osu, new ScoreInfo { OnlineID = 2 }, new TestArchiveReader()); var scoreManager = osu.Dependencies.Get(); // Note: A new score reference is used here since the import process mutates the original object to set an ID - Assert.That(scoreManager.IsAvailableLocally(new ScoreInfo { OnlineScoreID = 2 })); + Assert.That(scoreManager.IsAvailableLocally(new ScoreInfo { OnlineID = 2 })); } finally { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs index 80f807e7d3..744a2d187d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Multiplayer BeatmapInfo = beatmapInfo, User = new APIUser { Username = "Test user" }, Date = DateTimeOffset.Now, - OnlineScoreID = 12345, + OnlineID = 12345, Ruleset = rulesetInfo, }; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs index da1fa226e1..99b6edc3b6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Multiplayer BeatmapInfo = beatmapInfo, User = new APIUser { Username = "Test user" }, Date = DateTimeOffset.Now, - OnlineScoreID = 12345, + OnlineID = 12345, Ruleset = rulesetInfo, }; diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index c9dec25ad3..1653247570 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -128,7 +128,7 @@ namespace osu.Game.Tests.Visual.Navigation imported = Game.ScoreManager.Import(new ScoreInfo { Hash = Guid.NewGuid().ToString(), - OnlineScoreID = i, + OnlineID = i, BeatmapInfo = beatmap.Beatmaps.First(), Ruleset = ruleset ?? new OsuRuleset().RulesetInfo }).Result.Value; diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 38dd507e00..4ac3b7c733 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -51,13 +51,13 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => { - userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ }; + userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineID = currentScoreId++ }; bindHandler(userScore: userScore); }); createResults(() => userScore); - AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineScoreID).State == PanelState.Expanded); + AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded); } [Test] @@ -75,14 +75,14 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => { - userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ }; + userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineID = currentScoreId++ }; bindHandler(true, userScore); }); createResults(() => userScore); AddAssert("more than 1 panel displayed", () => this.ChildrenOfType().Count() > 1); - AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineScoreID).State == PanelState.Expanded); + AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded); } [Test] @@ -124,7 +124,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => { - userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ }; + userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineID = currentScoreId++ }; bindHandler(userScore: userScore); }); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index d22eff9e75..2363bbbfcf 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.UserInterface { var score = new ScoreInfo { - OnlineScoreID = i, + OnlineID = i, BeatmapInfo = beatmapInfo, BeatmapInfoID = beatmapInfo.ID, Accuracy = RNG.NextDouble(), diff --git a/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs index 467d5a9f23..057e98c421 100644 --- a/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs @@ -101,7 +101,7 @@ namespace osu.Game.Online.API.Requests.Responses BeatmapInfo = beatmap, User = User, Accuracy = Accuracy, - OnlineScoreID = OnlineID, + OnlineID = OnlineID, Date = Date, PP = PP, RulesetID = RulesetID, diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index 7bc3377ad9..05c9a1b6cf 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -69,7 +69,7 @@ namespace osu.Game.Online.Rooms var scoreInfo = new ScoreInfo { - OnlineScoreID = ID, + OnlineID = ID, TotalScore = TotalScore, MaxCombo = MaxCombo, BeatmapInfo = beatmap, diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index e09cc7c9cd..6320b7ff97 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -35,7 +35,7 @@ namespace osu.Game.Online var scoreInfo = new ScoreInfo { ID = TrackedItem.ID, - OnlineScoreID = TrackedItem.OnlineScoreID + OnlineID = TrackedItem.OnlineID }; if (Manager.IsAvailableLocally(scoreInfo)) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index b46c1a27ed..fefee370b9 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -80,12 +80,9 @@ namespace osu.Game.Scoring.Legacy byte[] compressedReplay = sr.ReadByteArray(); if (version >= 20140721) - scoreInfo.OnlineScoreID = sr.ReadInt64(); + scoreInfo.OnlineID = sr.ReadInt64(); else if (version >= 20121008) - scoreInfo.OnlineScoreID = sr.ReadInt32(); - - if (scoreInfo.OnlineID <= 0) - scoreInfo.OnlineScoreID = null; + scoreInfo.OnlineID = sr.ReadInt32(); if (compressedReplay?.Length > 0) { diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a0e9428cff..521cf7d1e9 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1031,13 +1031,13 @@ namespace osu.Game.Screens.Play // // Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint // conflicts across various systems (ie. solo and multiplayer). - long? onlineScoreId = score.ScoreInfo.OnlineScoreID; - score.ScoreInfo.OnlineScoreID = null; + long onlineScoreId = score.ScoreInfo.OnlineID; + score.ScoreInfo.OnlineID = -1; await scoreManager.Import(score.ScoreInfo, replayReader).ConfigureAwait(false); // ... And restore the online ID for other processes to handle correctly (e.g. de-duplication for the results screen). - score.ScoreInfo.OnlineScoreID = onlineScoreId; + score.ScoreInfo.OnlineID = onlineScoreId; } /// diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index c07cfa9c4d..c613167908 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -156,7 +156,7 @@ namespace osu.Game.Screens.Play request.Success += s => { - score.ScoreInfo.OnlineScoreID = s.ID; + score.ScoreInfo.OnlineID = s.ID; score.ScoreInfo.Position = s.Position; scoreSubmissionSource.SetResult(true); From bff02bedbfc28a94cab1f87a8e423c2fd653c29f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Dec 2021 16:01:41 +0900 Subject: [PATCH 290/419] Rename `APIScoreInfo` to `APIScore` --- .../Gameplay/TestSceneReplayDownloadButton.cs | 2 +- .../Visual/Online/TestSceneScoresContainer.cs | 20 +++++++++---------- .../Online/TestSceneUserProfileScores.cs | 8 ++++---- .../API/Requests/GetUserScoresRequest.cs | 2 +- .../{APIScoreInfo.cs => APIScore.cs} | 2 +- .../Responses/APIScoreWithPosition.cs | 2 +- .../Requests/Responses/APIScoresCollection.cs | 2 +- osu.Game/Online/Solo/SubmittableScore.cs | 2 +- .../Sections/Ranks/DrawableProfileScore.cs | 4 ++-- .../Ranks/DrawableProfileWeightedScore.cs | 2 +- .../Sections/Ranks/PaginatedScoreContainer.cs | 8 ++++---- 11 files changed, 27 insertions(+), 27 deletions(-) rename osu.Game/Online/API/Requests/Responses/{APIScoreInfo.cs => APIScore.cs} (99%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index f47fae33ca..42c4f89e9d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -164,7 +164,7 @@ namespace osu.Game.Tests.Visual.Gameplay private ScoreInfo getScoreInfo(bool replayAvailable) { - return new APIScoreInfo + return new APIScore { OnlineID = 2553163309, RulesetID = 0, diff --git a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs index 50969aad9b..be2db9a8a0 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs @@ -47,9 +47,9 @@ namespace osu.Game.Tests.Visual.Online var allScores = new APIScoresCollection { - Scores = new List + Scores = new List { - new APIScoreInfo + new APIScore { User = new APIUser { @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.Online TotalScore = 1234567890, Accuracy = 1, }, - new APIScoreInfo + new APIScore { User = new APIUser { @@ -98,7 +98,7 @@ namespace osu.Game.Tests.Visual.Online TotalScore = 1234789, Accuracy = 0.9997, }, - new APIScoreInfo + new APIScore { User = new APIUser { @@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Online TotalScore = 12345678, Accuracy = 0.9854, }, - new APIScoreInfo + new APIScore { User = new APIUser { @@ -143,7 +143,7 @@ namespace osu.Game.Tests.Visual.Online TotalScore = 1234567, Accuracy = 0.8765, }, - new APIScoreInfo + new APIScore { User = new APIUser { @@ -166,7 +166,7 @@ namespace osu.Game.Tests.Visual.Online var myBestScore = new APIScoreWithPosition { - Score = new APIScoreInfo + Score = new APIScore { User = new APIUser { @@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual.Online var myBestScoreWithNullPosition = new APIScoreWithPosition { - Score = new APIScoreInfo + Score = new APIScore { User = new APIUser { @@ -212,9 +212,9 @@ namespace osu.Game.Tests.Visual.Online var oneScore = new APIScoresCollection { - Scores = new List + Scores = new List { - new APIScoreInfo + new APIScore { User = new APIUser { diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs index 9c2cc13416..7dfdca8276 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Online { public TestSceneUserProfileScores() { - var firstScore = new APIScoreInfo + var firstScore = new APIScore { PP = 1047.21, Rank = ScoreRank.SH, @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Online Accuracy = 0.9813 }; - var secondScore = new APIScoreInfo + var secondScore = new APIScore { PP = 134.32, Rank = ScoreRank.A, @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online Accuracy = 0.998546 }; - var thirdScore = new APIScoreInfo + var thirdScore = new APIScore { PP = 96.83, Rank = ScoreRank.S, @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.Online Accuracy = 0.9726 }; - var noPPScore = new APIScoreInfo + var noPPScore = new APIScore { Rank = ScoreRank.B, Beatmap = new APIBeatmap diff --git a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs index e13ac8e539..653abf7427 100644 --- a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets; namespace osu.Game.Online.API.Requests { - public class GetUserScoresRequest : PaginatedAPIRequest> + public class GetUserScoresRequest : PaginatedAPIRequest> { private readonly long userId; private readonly ScoreType type; diff --git a/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APIScore.cs similarity index 99% rename from osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs rename to osu.Game/Online/API/Requests/Responses/APIScore.cs index 057e98c421..bd99a05a88 100644 --- a/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/APIScore.cs @@ -16,7 +16,7 @@ using osu.Game.Scoring.Legacy; namespace osu.Game.Online.API.Requests.Responses { - public class APIScoreInfo : IScoreInfo + public class APIScore : IScoreInfo { [JsonProperty(@"score")] public long TotalScore { get; set; } diff --git a/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs b/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs index 48b7134901..d3c9ba0c7e 100644 --- a/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs +++ b/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs @@ -14,7 +14,7 @@ namespace osu.Game.Online.API.Requests.Responses public int? Position; [JsonProperty(@"score")] - public APIScoreInfo Score; + public APIScore Score; public ScoreInfo CreateScoreInfo(RulesetStore rulesets, BeatmapInfo beatmap = null) { diff --git a/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs b/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs index 5304664bf8..283ebf2411 100644 --- a/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs +++ b/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs @@ -9,7 +9,7 @@ namespace osu.Game.Online.API.Requests.Responses public class APIScoresCollection { [JsonProperty(@"scores")] - public List Scores; + public List Scores; [JsonProperty(@"userScore")] public APIScoreWithPosition UserScore; diff --git a/osu.Game/Online/Solo/SubmittableScore.cs b/osu.Game/Online/Solo/SubmittableScore.cs index 373c302844..5ca5ad9619 100644 --- a/osu.Game/Online/Solo/SubmittableScore.cs +++ b/osu.Game/Online/Solo/SubmittableScore.cs @@ -16,7 +16,7 @@ namespace osu.Game.Online.Solo { /// /// A class specifically for sending scores to the API during score submission. - /// This is used instead of due to marginally different serialisation naming requirements. + /// This is used instead of due to marginally different serialisation naming requirements. /// [Serializable] public class SubmittableScore diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs index fb464e1b41..562be0403e 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs @@ -28,7 +28,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks private const float performance_background_shear = 0.45f; - protected readonly APIScoreInfo Score; + protected readonly APIScore Score; [Resolved] private OsuColour colours { get; set; } @@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks [Resolved] private OverlayColourProvider colourProvider { get; set; } - public DrawableProfileScore(APIScoreInfo score) + public DrawableProfileScore(APIScore score) { Score = score; diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs index e653be5cfa..78ae0a5634 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs @@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks { private readonly double weight; - public DrawableProfileWeightedScore(APIScoreInfo score, double weight) + public DrawableProfileWeightedScore(APIScore score, double weight) : base(score) { this.weight = weight; diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index c3f10587a9..5532e35cc5 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -15,7 +15,7 @@ using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; namespace osu.Game.Overlays.Profile.Sections.Ranks { - public class PaginatedScoreContainer : PaginatedProfileSubsection + public class PaginatedScoreContainer : PaginatedProfileSubsection { private readonly ScoreType type; @@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks } } - protected override void OnItemsReceived(List items) + protected override void OnItemsReceived(List items) { if (VisiblePages == 0) drawableItemIndex = 0; @@ -59,12 +59,12 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks base.OnItemsReceived(items); } - protected override APIRequest> CreateRequest() => + protected override APIRequest> CreateRequest() => new GetUserScoresRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage); private int drawableItemIndex; - protected override Drawable CreateDrawableItem(APIScoreInfo model) + protected override Drawable CreateDrawableItem(APIScore model) { switch (type) { From c6d0d6451d463767f60400143c6e195ed1c8f363 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Nov 2021 23:18:31 +0900 Subject: [PATCH 291/419] Change `IScoreInfo.User` to an interface type --- osu.Game/Online/API/Requests/Responses/APIScore.cs | 6 ++++++ osu.Game/Scoring/IScoreInfo.cs | 4 ++-- osu.Game/Scoring/ScoreInfo.cs | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIScore.cs b/osu.Game/Online/API/Requests/Responses/APIScore.cs index bd99a05a88..4f795bee6c 100644 --- a/osu.Game/Online/API/Requests/Responses/APIScore.cs +++ b/osu.Game/Online/API/Requests/Responses/APIScore.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; +using osu.Game.Users; namespace osu.Game.Online.API.Requests.Responses { @@ -150,6 +151,11 @@ namespace osu.Game.Online.API.Requests.Responses public IRulesetInfo Ruleset => new RulesetInfo { OnlineID = RulesetID }; IEnumerable IHasNamedFiles.Files => throw new NotImplementedException(); + #region Implementation of IScoreInfo + IBeatmapInfo IScoreInfo.Beatmap => Beatmap; + IUser IScoreInfo.User => User; + + #endregion } } diff --git a/osu.Game/Scoring/IScoreInfo.cs b/osu.Game/Scoring/IScoreInfo.cs index 8b5b228632..b4ad183cd3 100644 --- a/osu.Game/Scoring/IScoreInfo.cs +++ b/osu.Game/Scoring/IScoreInfo.cs @@ -4,14 +4,14 @@ using System; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; +using osu.Game.Users; namespace osu.Game.Scoring { public interface IScoreInfo : IHasOnlineID, IHasNamedFiles { - APIUser User { get; } + IUser User { get; } long TotalScore { get; } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index d6b7b2712b..7729e01389 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -14,6 +14,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; +using osu.Game.Users; using osu.Game.Utils; namespace osu.Game.Scoring @@ -261,6 +262,7 @@ namespace osu.Game.Scoring IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo; IRulesetInfo IScoreInfo.Ruleset => Ruleset; + IUser IScoreInfo.User => User; bool IScoreInfo.HasReplay => Files.Any(); #endregion From 970a9ae0748d8d6f99fd6035c5f96673702665a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Dec 2021 17:22:24 +0900 Subject: [PATCH 292/419] Add update thread asserts to `RoomManager` methods --- osu.Game/Screens/OnlinePlay/Components/RoomManager.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 02565c6ebe..6979b5bc30 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -7,11 +7,13 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; +using osu.Game.Utils; namespace osu.Game.Screens.OnlinePlay.Components { @@ -107,6 +109,7 @@ namespace osu.Game.Screens.OnlinePlay.Components public void AddOrUpdateRoom(Room room) { + Debug.Assert(ThreadSafety.IsUpdateThread); Debug.Assert(room.RoomID.Value != null); if (ignoredRooms.Contains(room.RoomID.Value.Value)) @@ -136,12 +139,16 @@ namespace osu.Game.Screens.OnlinePlay.Components public void RemoveRoom(Room room) { + Debug.Assert(ThreadSafety.IsUpdateThread); + rooms.Remove(room); notifyRoomsUpdated(); } public void ClearRooms() { + Debug.Assert(ThreadSafety.IsUpdateThread); + rooms.Clear(); notifyRoomsUpdated(); } From 73227c084e380d893bad1bb9a1d6e0fefcecccb2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Dec 2021 17:42:40 +0900 Subject: [PATCH 293/419] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 0c922c09ac..5cf59decec 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index adb25f46fe..6f01cc65fe 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index db5d9af865..fdb63a19d3 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From e98060ac37571471fa1477bd0038b64d219525db Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 10 Dec 2021 17:55:18 +0900 Subject: [PATCH 294/419] Remove unused using --- osu.Game/Screens/OnlinePlay/Components/RoomManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 6979b5bc30..238aa4059d 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -13,7 +13,6 @@ using osu.Framework.Logging; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; -using osu.Game.Utils; namespace osu.Game.Screens.OnlinePlay.Components { From 5f6e887be71bc8ebd5894f07669958d3e4445dd0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Dec 2021 18:11:52 +0900 Subject: [PATCH 295/419] Remove `OnlineID` comparison from `ScoreInfo.Equals` This matches the implementation we have for `BeatmapInfo` and `BeatmapSetInfo`. All comparisons of `OnlineID` should be done directly using them (ie. how it's done in `ScoreModelDownloader`). --- osu.Game.Tests/Scores/IO/TestScoreEquality.cs | 18 ------------------ osu.Game/Scoring/ScoreInfo.cs | 12 +++--------- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/Scores/IO/TestScoreEquality.cs b/osu.Game.Tests/Scores/IO/TestScoreEquality.cs index d1374eb6e5..42fcb3acab 100644 --- a/osu.Game.Tests/Scores/IO/TestScoreEquality.cs +++ b/osu.Game.Tests/Scores/IO/TestScoreEquality.cs @@ -44,24 +44,6 @@ namespace osu.Game.Tests.Scores.IO Assert.That(score1, Is.EqualTo(score2)); } - [Test] - public void TestNonMatchingByHash() - { - ScoreInfo score1 = new ScoreInfo { Hash = "a" }; - ScoreInfo score2 = new ScoreInfo { Hash = "b" }; - - Assert.That(score1, Is.Not.EqualTo(score2)); - } - - [Test] - public void TestMatchingByHash() - { - ScoreInfo score1 = new ScoreInfo { Hash = "a" }; - ScoreInfo score2 = new ScoreInfo { Hash = "a" }; - - Assert.That(score1, Is.EqualTo(score2)); - } - [Test] public void TestNonMatchingByNull() { diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 7729e01389..7c2d882f91 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -232,19 +232,13 @@ namespace osu.Game.Scoring public bool Equals(ScoreInfo other) { - if (other == null) - return false; + if (ReferenceEquals(this, other)) return true; + if (other == null) return false; if (ID != 0 && other.ID != 0) return ID == other.ID; - if (OnlineID > 0) - return OnlineID == other.OnlineID; - - if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash)) - return Hash == other.Hash; - - return ReferenceEquals(this, other); + return false; } #region Implementation of IHasOnlineID From c9f6c5c673aaff4ae53d4cce9ffb2832e49c31b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Dec 2021 18:32:06 +0900 Subject: [PATCH 296/419] Add `MatchesOnlineID` implementation for `IScoreInfo` --- osu.Game/Extensions/ModelExtensions.cs | 8 ++++++++ osu.Game/Online/ScoreDownloadTracker.cs | 3 ++- osu.Game/Scoring/ScoreModelDownloader.cs | 3 ++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Extensions/ModelExtensions.cs b/osu.Game/Extensions/ModelExtensions.cs index 2274da0fd4..f178a5c97b 100644 --- a/osu.Game/Extensions/ModelExtensions.cs +++ b/osu.Game/Extensions/ModelExtensions.cs @@ -104,6 +104,14 @@ namespace osu.Game.Extensions /// Whether online IDs match. If either instance is missing an online ID, this will return false. public static bool MatchesOnlineID(this APIUser? instance, APIUser? other) => matchesOnlineID(instance, other); + /// + /// Check whether the online ID of two s match. + /// + /// The instance to compare. + /// The other instance to compare against. + /// Whether online IDs match. If either instance is missing an online ID, this will return false. + public static bool MatchesOnlineID(this IScoreInfo? instance, IScoreInfo? other) => matchesOnlineID(instance, other); + private static bool matchesOnlineID(this IHasOnlineID? instance, IHasOnlineID? other) { if (instance == null || other == null) diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index 6320b7ff97..68932cc388 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Scoring; @@ -113,7 +114,7 @@ namespace osu.Game.Online UpdateState(DownloadState.NotDownloaded); }); - private bool checkEquality(IScoreInfo x, IScoreInfo y) => x.OnlineID == y.OnlineID; + private bool checkEquality(IScoreInfo x, IScoreInfo y) => x.MatchesOnlineID(y); #region Disposal diff --git a/osu.Game/Scoring/ScoreModelDownloader.cs b/osu.Game/Scoring/ScoreModelDownloader.cs index 038a4bc351..514b7a57de 100644 --- a/osu.Game/Scoring/ScoreModelDownloader.cs +++ b/osu.Game/Scoring/ScoreModelDownloader.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -17,6 +18,6 @@ namespace osu.Game.Scoring protected override ArchiveDownloadRequest CreateDownloadRequest(IScoreInfo score, bool minimiseDownload) => new DownloadReplayRequest(score); public override ArchiveDownloadRequest GetExistingDownload(IScoreInfo model) - => CurrentDownloads.Find(r => r.Model.OnlineID == model.OnlineID); + => CurrentDownloads.Find(r => r.Model.MatchesOnlineID(model)); } } From f7c5a3f506815eee95fc36e62fb88e411319f8ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Dec 2021 18:32:32 +0900 Subject: [PATCH 297/419] Use similar method of consuming `OnlineID` as done in beatmap classes --- osu.Game/Database/OsuDbContext.cs | 2 +- osu.Game/Scoring/ScoreInfo.cs | 16 +++++++++------- osu.Game/Screens/Play/Player.cs | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index 13d362e0be..7cd9ae2885 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -147,7 +147,7 @@ namespace osu.Game.Database modelBuilder.Entity().HasOne(b => b.BaseDifficulty); - modelBuilder.Entity().HasIndex(b => b.OnlineScoreID).IsUnique(); + modelBuilder.Entity().HasIndex(b => b.OnlineID).IsUnique(); } private class OsuDbLoggerFactory : ILoggerFactory diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 7c2d882f91..7acc7bd055 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -137,7 +137,14 @@ namespace osu.Game.Scoring [Column("Beatmap")] public BeatmapInfo BeatmapInfo { get; set; } - public long? OnlineScoreID { get; set; } + private long? onlineID; + + [Column("OnlineScoreID")] + public long? OnlineID + { + get => onlineID; + set => onlineID = value > 0 ? value : null; + } public DateTimeOffset Date { get; set; } @@ -243,12 +250,7 @@ namespace osu.Game.Scoring #region Implementation of IHasOnlineID - [NotMapped] - public long OnlineID - { - get => OnlineScoreID ?? -1; - set => OnlineScoreID = value; - } + long IHasOnlineID.OnlineID => OnlineID ?? -1; #endregion diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 521cf7d1e9..e2896c174f 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1031,7 +1031,7 @@ namespace osu.Game.Screens.Play // // Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint // conflicts across various systems (ie. solo and multiplayer). - long onlineScoreId = score.ScoreInfo.OnlineID; + long? onlineScoreId = score.ScoreInfo.OnlineID; score.ScoreInfo.OnlineID = -1; await scoreManager.Import(score.ScoreInfo, replayReader).ConfigureAwait(false); From 3b899af061f2e3f450bbec9913c32c6167681b53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Dec 2021 18:37:57 +0900 Subject: [PATCH 298/419] Update libraries --- .config/dotnet-tools.json | 4 ++-- osu.Game/osu.Game.csproj | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 6444127594..985fc09df3 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -27,10 +27,10 @@ ] }, "ppy.localisationanalyser.tools": { - "version": "2021.725.0", + "version": "2021.1210.0", "commands": [ "localisation" ] } } -} \ No newline at end of file +} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 6f01cc65fe..46064e320b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -20,7 +20,7 @@ - + @@ -31,15 +31,15 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + From 5e9510be3608a3718c9b0d109cc8ae80f6b44add Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Dec 2021 18:57:33 +0900 Subject: [PATCH 299/419] Add test coverage of editor resetting mods on entering --- osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index 160af47a6d..50794f15ed 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -9,6 +9,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; @@ -44,6 +45,7 @@ namespace osu.Game.Tests.Visual.Editing protected override void LoadEditor() { Beatmap.Value = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(b => b.RulesetID == 0)); + SelectedMods.Value = new[] { new ModCinema() }; base.LoadEditor(); } @@ -67,6 +69,7 @@ namespace osu.Game.Tests.Visual.Editing var background = this.ChildrenOfType().Single(); return background.Colour == Color4.DarkGray && background.BlurAmount.Value == 0; }); + AddAssert("no mods selected", () => SelectedMods.Value.Count == 0); } [Test] From c1b3ee6bb23765a540708dd5af9f13064f310786 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Dec 2021 18:57:43 +0900 Subject: [PATCH 300/419] Fix editor not resetting mods when entering Would leave the user potentially in a test mode that is in a weird state (ie. if cinema mod was enabled). Eventually we'll add the ability to choose mods for test play, but that will be done in a saner way. Closes #15870. --- osu.Game/Screens/Edit/EditorLoader.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index 2a01a5b6b2..27ae8ae497 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -9,6 +10,7 @@ using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; @@ -53,6 +55,14 @@ namespace osu.Game.Screens.Edit }); } + protected override void LoadComplete() + { + base.LoadComplete(); + + // will be restored via lease, see `DisallowExternalBeatmapRulesetChanges`. + Mods.Value = ArraySegment.Empty; + } + protected virtual Editor CreateEditor() => new Editor(this); protected override void LogoArriving(OsuLogo logo, bool resuming) From 5a953f38119cdca4a3a6a214110b2d2f934ab013 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Dec 2021 19:14:33 +0900 Subject: [PATCH 301/419] Fix autopilot not working as expected on touch devices Closes https://github.com/ppy/osu/issues/12731. I haven't tested this, but quite confident it should work. Will test later today unless someone else beats me. --- osu.Game.Rulesets.Osu/OsuInputManager.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs index 7314021a14..f5fc3de381 100644 --- a/osu.Game.Rulesets.Osu/OsuInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.ComponentModel; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Input.StateChanges.Events; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu @@ -39,6 +40,17 @@ namespace osu.Game.Rulesets.Osu return base.Handle(e); } + protected override bool HandleMouseTouchStateChange(TouchStateChangeEvent e) + { + if (!AllowUserCursorMovement) + { + // Still allow for forwarding of the "touch" part, but block the positional data. + e = new TouchStateChangeEvent(e.State, e.Input, e.Touch, false, null); + } + + return base.HandleMouseTouchStateChange(e); + } + private class OsuKeyBindingContainer : RulesetKeyBindingContainer { public bool AllowUserPresses = true; From 6057037e355b06eb763609200c06adefa99e31c2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 10 Dec 2021 20:08:59 +0900 Subject: [PATCH 302/419] Move playlist item beatmap population to MatchSubScreen --- .../StatefulMultiplayerClientTest.cs | 4 -- .../TestSceneAllPlayersQueueMode.cs | 16 ++--- .../Multiplayer/TestSceneHostOnlyQueueMode.cs | 10 +-- .../Multiplayer/TestSceneMultiplayer.cs | 2 +- .../Multiplayer/TestSceneMultiplayerPlayer.cs | 7 +- .../TestSceneMultiplayerQueueList.cs | 24 +++---- .../Online/Multiplayer/MultiplayerClient.cs | 65 +++++-------------- .../Multiplayer/OnlineMultiplayerClient.cs | 2 +- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- .../Match/MultiplayerReadyButton.cs | 10 ++- .../Multiplayer/MultiplayerMatchSongSelect.cs | 6 +- .../Multiplayer/MultiplayerMatchSubScreen.cs | 49 ++++++++++++-- .../Multiplayer/TestMultiplayerClient.cs | 2 +- 13 files changed, 103 insertions(+), 96 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 42305ccd81..bc0041e2c2 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -45,8 +45,6 @@ namespace osu.Game.Tests.NonVisual.Multiplayer AddRepeatStep("add some users", () => Client.AddUser(new APIUser { Id = id++ }), 5); checkPlayingUserCount(0); - AddAssert("playlist item is available", () => Client.CurrentMatchPlayingItem.Value != null); - changeState(3, MultiplayerUserState.WaitingForLoad); checkPlayingUserCount(3); @@ -64,8 +62,6 @@ namespace osu.Game.Tests.NonVisual.Multiplayer AddStep("leave room", () => Client.LeaveRoom()); checkPlayingUserCount(0); - - AddAssert("playlist item is null", () => Client.CurrentMatchPlayingItem.Value == null); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs index 8373979308..4bf38c9ff8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestFirstItemSelectedByDefault() { - AddAssert("first item selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[0].ID); + AddAssert("first item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID); } [Test] @@ -27,13 +27,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { addItem(() => OtherBeatmap); AddAssert("playlist has 2 items", () => Client.APIRoom?.Playlist.Count == 2); - AddAssert("last playlist item is different", () => Client.APIRoom?.Playlist[1].Beatmap.Value.OnlineID == OtherBeatmap.OnlineID); addItem(() => InitialBeatmap); AddAssert("playlist has 3 items", () => Client.APIRoom?.Playlist.Count == 3); - AddAssert("last playlist item is different", () => Client.APIRoom?.Playlist[2].Beatmap.Value.OnlineID == InitialBeatmap.OnlineID); - AddAssert("first item still selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[0].ID); + AddAssert("first item still selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID); } [Test] @@ -43,7 +41,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("playlist has only one item", () => Client.APIRoom?.Playlist.Count == 1); AddAssert("playlist item is expired", () => Client.APIRoom?.Playlist[0].Expired == true); - AddAssert("last item selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[0].ID); + AddAssert("last item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID); } [Test] @@ -55,12 +53,12 @@ namespace osu.Game.Tests.Visual.Multiplayer RunGameplay(); AddAssert("first item expired", () => Client.APIRoom?.Playlist[0].Expired == true); - AddAssert("next item selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[1].ID); + AddAssert("next item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[1].ID); RunGameplay(); AddAssert("second item expired", () => Client.APIRoom?.Playlist[1].Expired == true); - AddAssert("next item selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[2].ID); + AddAssert("next item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[2].ID); } [Test] @@ -74,8 +72,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("change queue mode", () => Client.ChangeSettings(queueMode: QueueMode.HostOnly)); AddAssert("playlist has 3 items", () => Client.APIRoom?.Playlist.Count == 3); - AddAssert("playlist item is the other beatmap", () => Client.CurrentMatchPlayingItem.Value?.BeatmapID == OtherBeatmap.OnlineID); - AddAssert("playlist item is not expired", () => Client.APIRoom?.Playlist[1].Expired == false); + AddAssert("item 2 is not expired", () => Client.APIRoom?.Playlist[1].Expired == false); + AddAssert("current item is the other beatmap", () => Client.Room?.Settings.PlaylistItemId == 2); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs index ccac3de304..c06bd8fdbe 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestFirstItemSelectedByDefault() { - AddAssert("first item selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[0].ID); + AddAssert("first item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID); } [Test] @@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { selectNewItem(() => InitialBeatmap); - AddAssert("playlist item still selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[0].ID); + AddAssert("playlist item still selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID); } [Test] @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { selectNewItem(() => OtherBeatmap); - AddAssert("playlist item still selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[0].ID); + AddAssert("playlist item still selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID); } [Test] @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("playlist contains two items", () => Client.APIRoom?.Playlist.Count == 2); AddAssert("first playlist item expired", () => Client.APIRoom?.Playlist[0].Expired == true); AddAssert("second playlist item not expired", () => Client.APIRoom?.Playlist[1].Expired == false); - AddAssert("second playlist item selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[1].ID); + AddAssert("second playlist item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[1].ID); } [Test] @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(otherBeatmap = beatmap())); AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen); - AddUntilStep("selected item is new beatmap", () => Client.CurrentMatchPlayingItem.Value?.Beatmap.Value?.OnlineID == otherBeatmap.OnlineID); + AddUntilStep("selected item is new beatmap", () => (CurrentSubScreen as MultiplayerMatchSubScreen)?.SelectedItem.Value.BeatmapID == otherBeatmap.OnlineID); } private void addItem(Func beatmap) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 5eb0abc830..f20ab914e2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -416,7 +416,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("Enter song select", () => { var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerScreenStack.CurrentScreen).CurrentSubScreen; - ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.CurrentMatchPlayingItem.Value); + ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.Room?.Settings.PlaylistItemId); }); AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 5708b2f789..73f2ed5b39 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Multiplayer; @@ -27,7 +28,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("initialise gameplay", () => { - Stack.Push(player = new MultiplayerPlayer(Client.APIRoom, Client.CurrentMatchPlayingItem.Value, Client.Room?.Users.ToArray())); + Stack.Push(player = new MultiplayerPlayer(Client.APIRoom, new PlaylistItem + { + Beatmap = { Value = Beatmap.Value.BeatmapInfo }, + Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset } + }, Client.Room?.Users.ToArray())); }); AddUntilStep("wait for player to be current", () => player.IsCurrentScreen() && player.IsLoaded); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index a2b2da0aec..61a92c32a4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; @@ -27,11 +28,12 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayerQueueList : MultiplayerTestScene { - private MultiplayerQueueList playlist; + private readonly Bindable selectedItem = new Bindable(); [Cached(typeof(UserLookupCache))] private readonly TestUserLookupCache userLookupCache = new TestUserLookupCache(); + private MultiplayerQueueList playlist; private BeatmapManager beatmaps; private RulesetStore rulesets; private BeatmapSetInfo importedSet; @@ -50,12 +52,14 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create playlist", () => { + selectedItem.Value = null; + Child = playlist = new MultiplayerQueueList { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(500, 300), - SelectedItem = { BindTarget = Client.CurrentMatchPlayingItem }, + SelectedItem = { BindTarget = selectedItem }, Items = { BindTarget = Client.APIRoom!.Playlist } }; }); @@ -107,22 +111,14 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); - assertDeleteButtonVisibility(0, false); - addPlaylistItem(() => API.LocalUser.Value.OnlineID); + + AddStep("select item 0", () => selectedItem.Value = playlist.ChildrenOfType>().ElementAt(0).Model); assertDeleteButtonVisibility(0, false); assertDeleteButtonVisibility(1, true); - // Run through gameplay. - AddStep("set state to ready", () => Client.ChangeUserState(API.LocalUser.Value.Id, MultiplayerUserState.Ready)); - AddUntilStep("local state is ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready); - AddStep("start match", () => Client.StartMatch()); - AddUntilStep("match started", () => Client.LocalUser?.State == MultiplayerUserState.WaitingForLoad); - AddStep("set state to loaded", () => Client.ChangeUserState(API.LocalUser.Value.Id, MultiplayerUserState.Loaded)); - AddUntilStep("local state is playing", () => Client.LocalUser?.State == MultiplayerUserState.Playing); - AddStep("set state to finished play", () => Client.ChangeUserState(API.LocalUser.Value.Id, MultiplayerUserState.FinishedPlay)); - AddUntilStep("local state is results", () => Client.LocalUser?.State == MultiplayerUserState.Results); - + AddStep("select item 1", () => selectedItem.Value = playlist.ChildrenOfType>().ElementAt(1).Model); + assertDeleteButtonVisibility(0, true); assertDeleteButtonVisibility(1, false); } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 55b4def908..f366de557f 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -95,8 +95,6 @@ namespace osu.Game.Online.Multiplayer protected readonly BindableList PlayingUserIds = new BindableList(); - public readonly Bindable CurrentMatchPlayingItem = new Bindable(); - /// /// The corresponding to the local player, if available. /// @@ -162,9 +160,6 @@ namespace osu.Game.Online.Multiplayer var joinedRoom = await JoinRoom(room.RoomID.Value.Value, password ?? room.Password.Value).ConfigureAwait(false); Debug.Assert(joinedRoom != null); - // Populate playlist items. - var playlistItems = await Task.WhenAll(joinedRoom.Playlist.Select(item => createPlaylistItem(item, item.ID == joinedRoom.Settings.PlaylistItemId))).ConfigureAwait(false); - // Populate users. Debug.Assert(joinedRoom.Users != null); await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)).ConfigureAwait(false); @@ -176,7 +171,7 @@ namespace osu.Game.Online.Multiplayer APIRoom = room; APIRoom.Playlist.Clear(); - APIRoom.Playlist.AddRange(playlistItems); + APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem)); Debug.Assert(LocalUser != null); addUserToAPIRoom(LocalUser); @@ -219,7 +214,6 @@ namespace osu.Game.Online.Multiplayer { APIRoom = null; Room = null; - CurrentMatchPlayingItem.Value = null; PlayingUserIds.Clear(); RoomUpdated?.Invoke(); @@ -477,28 +471,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(APIRoom != null); Debug.Assert(Room != null); - Scheduler.Add(() => - { - // ensure the new selected item is populated immediately. - var playlistItem = APIRoom.Playlist.Single(p => p.ID == newSettings.PlaylistItemId); - - if (playlistItem != null) - { - GetAPIBeatmap(playlistItem.BeatmapID).ContinueWith(b => - { - // Should be called outside of the `Scheduler` logic (and specifically accessing `Exception`) to suppress an exception from firing outwards. - bool success = b.Exception == null; - - Scheduler.Add(() => - { - if (success) - playlistItem.Beatmap.Value = b.Result; - - updateLocalRoomSettings(newSettings); - }); - }); - } - }); + Scheduler.Add(() => updateLocalRoomSettings(newSettings)); return Task.CompletedTask; } @@ -653,12 +626,10 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } - public async Task PlaylistItemAdded(MultiplayerPlaylistItem item) + public Task PlaylistItemAdded(MultiplayerPlaylistItem item) { if (Room == null) - return; - - var playlistItem = await createPlaylistItem(item, true).ConfigureAwait(false); + return Task.CompletedTask; Scheduler.Add(() => { @@ -668,11 +639,13 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(APIRoom != null); Room.Playlist.Add(item); - APIRoom.Playlist.Add(playlistItem); + APIRoom.Playlist.Add(createPlaylistItem(item)); ItemAdded?.Invoke(item); RoomUpdated?.Invoke(); }); + + return Task.CompletedTask; } public Task PlaylistItemRemoved(long playlistItemId) @@ -697,12 +670,10 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } - public async Task PlaylistItemChanged(MultiplayerPlaylistItem item) + public Task PlaylistItemChanged(MultiplayerPlaylistItem item) { if (Room == null) - return; - - var playlistItem = await createPlaylistItem(item, true).ConfigureAwait(false); + return Task.CompletedTask; Scheduler.Add(() => { @@ -715,15 +686,13 @@ namespace osu.Game.Online.Multiplayer int existingIndex = APIRoom.Playlist.IndexOf(APIRoom.Playlist.Single(existing => existing.ID == item.ID)); APIRoom.Playlist.RemoveAt(existingIndex); - APIRoom.Playlist.Insert(existingIndex, playlistItem); - - // If the currently-selected item was the one that got replaced, update the selected item to the new one. - if (CurrentMatchPlayingItem.Value?.ID == playlistItem.ID) - CurrentMatchPlayingItem.Value = playlistItem; + APIRoom.Playlist.Insert(existingIndex, createPlaylistItem(item)); ItemChanged?.Invoke(item); RoomUpdated?.Invoke(); }); + + return Task.CompletedTask; } /// @@ -752,12 +721,11 @@ namespace osu.Game.Online.Multiplayer APIRoom.Password.Value = Room.Settings.Password; APIRoom.Type.Value = Room.Settings.MatchType; APIRoom.QueueMode.Value = Room.Settings.QueueMode; - RoomUpdated?.Invoke(); - CurrentMatchPlayingItem.Value = APIRoom.Playlist.SingleOrDefault(p => p.ID == settings.PlaylistItemId); + RoomUpdated?.Invoke(); } - private async Task createPlaylistItem(MultiplayerPlaylistItem item, bool populateBeatmapImmediately) + private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) { var ruleset = Rulesets.GetRuleset(item.RulesetID); @@ -779,9 +747,6 @@ namespace osu.Game.Online.Multiplayer playlistItem.RequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))); playlistItem.AllowedMods.AddRange(item.AllowedMods.Select(m => m.ToMod(rulesetInstance))); - if (populateBeatmapImmediately) - playlistItem.Beatmap.Value = await GetAPIBeatmap(item.BeatmapID).ConfigureAwait(false); - return playlistItem; } @@ -791,7 +756,7 @@ namespace osu.Game.Online.Multiplayer /// The beatmap ID. /// A token to cancel the request. /// The retrieval task. - protected abstract Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default); + public abstract Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default); /// /// For the provided user ID, update whether the user is included in . diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index d268d2bf69..f911ef3121 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -178,7 +178,7 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); } - protected override Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default) + public override Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default) { return beatmapLookupCache.GetBeatmapAsync(beatmapId, cancellationToken); } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 184ac2c563..55f5622afd 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Match public abstract class RoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner { [Cached(typeof(IBindable))] - protected readonly Bindable SelectedItem = new Bindable(); + public readonly Bindable SelectedItem = new Bindable(); public override bool? AllowTrackAdjustments => true; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 874113d859..06959d942f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -63,6 +63,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match sampleUnready = audio.Samples.Get(@"Multiplayer/player-unready"); } + protected override void LoadComplete() + { + base.LoadComplete(); + + SelectedItem.BindValueChanged(_ => updateState()); + } + protected override void OnRoomUpdated() { base.OnRoomUpdated(); @@ -104,7 +111,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match bool enableButton = Room?.State == MultiplayerRoomState.Open - && Client.CurrentMatchPlayingItem.Value?.Expired == false + && SelectedItem.Value?.ID == Room.Settings.PlaylistItemId + && !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired && !operationInProgress.Value; // When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 8d3686dd6d..073497e1ce 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } - private readonly PlaylistItem itemToEdit; + private readonly long? itemToEdit; private LoadingLayer loadingLayer; @@ -36,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer /// The item to be edited. May be null, in which case a new item will be added to the playlist. /// An optional initial beatmap selection to perform. /// An optional initial ruleset selection to perform. - public MultiplayerMatchSongSelect(Room room, PlaylistItem itemToEdit = null, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null) + public MultiplayerMatchSongSelect(Room room, long? itemToEdit = null, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null) : base(room) { this.itemToEdit = itemToEdit; @@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer var multiplayerItem = new MultiplayerPlaylistItem { - ID = itemToEdit?.ID ?? 0, + ID = itemToEdit ?? 0, BeatmapID = item.BeatmapID, BeatmapChecksum = item.Beatmap.Value.MD5Hash, RulesetID = item.RulesetID, diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 946c749db3..3543836d8d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -67,8 +68,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadComplete(); - SelectedItem.BindTo(client.CurrentMatchPlayingItem); - BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true); UserMods.BindValueChanged(onUserModsChanged); @@ -147,7 +146,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new MultiplayerPlaylist { RelativeSizeAxes = Axes.Both, - RequestEdit = OpenSongSelection + RequestEdit = item => OpenSongSelection(item.ID) } }, new[] @@ -224,7 +223,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer /// Opens the song selection screen to add or edit an item. /// /// An optional playlist item to edit. If null, a new item will be added instead. - internal void OpenSongSelection([CanBeNull] PlaylistItem itemToEdit = null) + internal void OpenSongSelection(long? itemToEdit = null) { if (!this.IsCurrentScreen()) return; @@ -389,11 +388,48 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return; } + updateCurrentItem(); + addItemButton.Alpha = client.IsHost || Room.QueueMode.Value != QueueMode.HostOnly ? 1 : 0; Scheduler.AddOnce(UpdateMods); } + private void updateCurrentItem() + { + Debug.Assert(client.Room != null); + + var expectedSelectedItem = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId); + + if (expectedSelectedItem == null) + return; + + if (SelectedItem.Value?.Equals(expectedSelectedItem) == true && expectedSelectedItem.Beatmap.Value != null) + return; + + SelectedItem.Value = null; + + if (expectedSelectedItem.Beatmap.Value == null) + { + Task.Run(async () => + { + var beatmap = await client.GetAPIBeatmap(expectedSelectedItem.BeatmapID).ConfigureAwait(false); + + Schedule(() => + { + expectedSelectedItem.Beatmap.Value = beatmap; + + if (Room.Playlist.SingleOrDefault(i => i.ID == client.Room?.Settings.PlaylistItemId)?.Equals(expectedSelectedItem) == true) + applyCurrentItem(); + }); + }); + } + else + applyCurrentItem(); + + void applyCurrentItem() => SelectedItem.Value = expectedSelectedItem; + } + private void handleRoomLost() => Schedule(() => { if (this.IsCurrentScreen()) @@ -446,6 +482,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (!this.IsCurrentScreen()) return; + if (client.Room == null) + return; + if (!client.IsHost) { // todo: should handle this when the request queue is implemented. @@ -454,7 +493,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return; } - this.Push(new MultiplayerMatchSongSelect(Room, SelectedItem.Value, beatmap, ruleset)); + this.Push(new MultiplayerMatchSongSelect(Room, client.Room.Settings.PlaylistItemId, beatmap, ruleset)); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index d22f0415e6..d20d6b1d37 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -376,7 +376,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, playlistItemId); - protected override Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default) + public override Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default) { IBeatmapSetInfo? set = roomManager.ServerSideRooms.SelectMany(r => r.Playlist) .FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value.BeatmapSet From bc1f1f35b5a73dde39bfaab211300df330f5f7f3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 Dec 2021 16:40:51 +0300 Subject: [PATCH 303/419] Remove now redundant inclusion of `TouchMoveEvent` in `OsuInputManager.Handle` Now it's handled separately via the `HandleMouseTouchStateChange` override. --- osu.Game.Rulesets.Osu/OsuInputManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs index f5fc3de381..57704b3bd8 100644 --- a/osu.Game.Rulesets.Osu/OsuInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu protected override bool Handle(UIEvent e) { - if ((e is MouseMoveEvent || e is TouchMoveEvent) && !AllowUserCursorMovement) return false; + if (e is MouseMoveEvent && !AllowUserCursorMovement) return false; return base.Handle(e); } From cf3041128888b4be5993728e8604c90df1cdd49b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 Dec 2021 17:13:13 +0300 Subject: [PATCH 304/419] Revert "Remove now redundant inclusion of `TouchMoveEvent` in `OsuInputManager.Handle`" This reverts commit bc1f1f35b5a73dde39bfaab211300df330f5f7f3. --- osu.Game.Rulesets.Osu/OsuInputManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs index 57704b3bd8..f5fc3de381 100644 --- a/osu.Game.Rulesets.Osu/OsuInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu protected override bool Handle(UIEvent e) { - if (e is MouseMoveEvent && !AllowUserCursorMovement) return false; + if ((e is MouseMoveEvent || e is TouchMoveEvent) && !AllowUserCursorMovement) return false; return base.Handle(e); } From 63a017bc8e43bbdc61db396047e100eaf9814d80 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 11 Dec 2021 19:33:37 +0900 Subject: [PATCH 305/419] Use Array.Empty instead --- osu.Game/Screens/Edit/EditorLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index 27ae8ae497..15d70e28b6 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.Edit base.LoadComplete(); // will be restored via lease, see `DisallowExternalBeatmapRulesetChanges`. - Mods.Value = ArraySegment.Empty; + Mods.Value = Array.Empty(); } protected virtual Editor CreateEditor() => new Editor(this); From 25b274c323737c73de6d45d55a57096eec2faca8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 11 Dec 2021 22:47:08 +0900 Subject: [PATCH 306/419] Fix starting gameplay too early after import --- .../Multiplayer/TestSceneMultiplayer.cs | 40 +++++++++++-------- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 8 ++-- .../Multiplayer/MultiplayerMatchSubScreen.cs | 21 ++++++++-- 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index f20ab914e2..bc2902480d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -391,9 +391,9 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddStep("set user ready", () => client.ChangeState(MultiplayerUserState.Ready)); - AddStep("delete beatmap", () => beatmaps.Delete(importedSet)); + pressReadyButton(); + AddStep("delete beatmap", () => beatmaps.Delete(importedSet)); AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle); } @@ -413,6 +413,8 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); + pressReadyButton(); + AddStep("Enter song select", () => { var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerScreenStack.CurrentScreen).CurrentSubScreen; @@ -592,19 +594,8 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value); - - AddStep("click ready button", () => - { - InputManager.MoveMouseTo(readyButton); - InputManager.Click(MouseButton.Left); - }); - - AddUntilStep("wait for player to be ready", () => client.Room?.Users[0].State == MultiplayerUserState.Ready); - AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value); - - AddStep("click start button", () => InputManager.Click(MouseButton.Left)); - + pressReadyButton(); + pressReadyButton(); AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player); // Gameplay runs in real-time, so we need to incrementally check if gameplay has finished in order to not time out. @@ -665,7 +656,24 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } - private MultiplayerReadyButton readyButton => this.ChildrenOfType().Single(); + private ReadyButton readyButton => this.ChildrenOfType().Single(); + + private void pressReadyButton() + { + AddUntilStep("wait for ready button to be enabled", () => readyButton.Enabled.Value); + + MultiplayerUserState lastState = MultiplayerUserState.Idle; + + AddStep("click ready button", () => + { + lastState = client.LocalUser?.State ?? MultiplayerUserState.Idle; + + InputManager.MoveMouseTo(readyButton); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("wait for state change", () => client.LocalUser?.State != lastState); + } private void createRoom(Func room) { diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 55f5622afd..e5a6915ca3 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -302,7 +302,7 @@ namespace osu.Game.Screens.OnlinePlay.Match public override void OnResuming(IScreen last) { base.OnResuming(last); - updateWorkingBeatmap(); + UpdateWorkingBeatmap(); beginHandlingTrack(); Scheduler.AddOnce(UpdateMods); } @@ -345,7 +345,7 @@ namespace osu.Game.Screens.OnlinePlay.Match private void selectedItemChanged() { - updateWorkingBeatmap(); + UpdateWorkingBeatmap(); var selected = SelectedItem.Value; @@ -374,9 +374,9 @@ namespace osu.Game.Screens.OnlinePlay.Match } } - private void beatmapUpdated(BeatmapSetInfo set) => Schedule(updateWorkingBeatmap); + private void beatmapUpdated(BeatmapSetInfo set) => Schedule(UpdateWorkingBeatmap); - private void updateWorkingBeatmap() + protected virtual void UpdateWorkingBeatmap() { var beatmap = SelectedItem.Value?.Beatmap.Value; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 3543836d8d..b517142069 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -15,6 +15,7 @@ using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Extensions; using osu.Game.Online; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -326,10 +327,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.LocalUser?.State == MultiplayerUserState.Ready) client.ChangeState(MultiplayerUserState.Idle); } - else + } + + protected override void UpdateWorkingBeatmap() + { + var lastBeatmap = Beatmap.Value; + + base.UpdateWorkingBeatmap(); + + if (Beatmap.Value.BeatmapInfo.MatchesOnlineID(lastBeatmap.BeatmapInfo)) + return; + + if (!Beatmap.Value.BeatmapInfo.MatchesOnlineID(SelectedItem.Value?.Beatmap.Value)) + return; + + if (client.LocalUser?.State == MultiplayerUserState.Spectating + && (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing)) { - if (client.LocalUser?.State == MultiplayerUserState.Spectating && (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing)) - onLoadRequested(); + onLoadRequested(); } } From ece2cddb7fd11171450b336bcf049efff3b28d38 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 11 Dec 2021 22:51:20 +0900 Subject: [PATCH 307/419] Fix DrawableRoomPlaylistItem lookup interfering with tests --- .../Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 8042f7d772..a877c6aca9 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -24,6 +25,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osu.Game.Online.Chat; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays.BeatmapSet; using osu.Game.Rulesets; @@ -90,6 +92,10 @@ namespace osu.Game.Screens.OnlinePlay [Resolved] private UserLookupCache userLookupCache { get; set; } + [CanBeNull] + [Resolved(CanBeNull = true)] + private MultiplayerClient multiplayerClient { get; set; } + [Resolved] private BeatmapLookupCache beatmapLookupCache { get; set; } @@ -157,7 +163,13 @@ namespace osu.Game.Screens.OnlinePlay if (Item.Beatmap.Value == null) { - var foundBeatmap = await beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ConfigureAwait(false); + IBeatmapInfo foundBeatmap; + + if (multiplayerClient != null) + foundBeatmap = await multiplayerClient.GetAPIBeatmap(Item.BeatmapID).ConfigureAwait(false); + else + foundBeatmap = await beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ConfigureAwait(false); + Schedule(() => Item.Beatmap.Value = foundBeatmap); } } From 99cd36d2f611eea89ae74591b6ba37fb16cac9f9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 11 Dec 2021 22:52:06 +0900 Subject: [PATCH 308/419] Resolve some test failures due to async population --- .../Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs | 2 +- osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs index 4bf38c9ff8..a5744f9986 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestCorrectItemSelectedAfterNewItemAdded() { addItem(() => OtherBeatmap); - AddAssert("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); + AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); } private void addItem(Func beatmap) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs index c06bd8fdbe..ac1efdda4a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(otherBeatmap = beatmap())); AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen); - AddUntilStep("selected item is new beatmap", () => (CurrentSubScreen as MultiplayerMatchSubScreen)?.SelectedItem.Value.BeatmapID == otherBeatmap.OnlineID); + AddUntilStep("selected item is new beatmap", () => (CurrentSubScreen as MultiplayerMatchSubScreen)?.SelectedItem.Value?.BeatmapID == otherBeatmap.OnlineID); } private void addItem(Func beatmap) From d6c08fae48dccfd80d2bd003f3b8119130a171ca Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 11 Dec 2021 23:08:17 +0900 Subject: [PATCH 309/419] Fix incorrect global beatmap with availability changes --- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 7 +++++++ .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 ++ 2 files changed, 9 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index e5a6915ca3..6b395058fa 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Online; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -378,6 +379,12 @@ namespace osu.Game.Screens.OnlinePlay.Match protected virtual void UpdateWorkingBeatmap() { + if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable) + { + Beatmap.Value = beatmapManager.GetWorkingBeatmap(null); + return; + } + var beatmap = SelectedItem.Value?.Beatmap.Value; // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index b517142069..65190101b8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -327,6 +327,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.LocalUser?.State == MultiplayerUserState.Ready) client.ChangeState(MultiplayerUserState.Idle); } + + UpdateWorkingBeatmap(); } protected override void UpdateWorkingBeatmap() From 9f792fec49366e8ac3cc9d31a4f8c0bc214d50c6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 12 Dec 2021 16:11:48 +0900 Subject: [PATCH 310/419] Fix test failures from async item loading --- .../Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs index ac1efdda4a..c7eeff81fe 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs @@ -9,6 +9,7 @@ using osu.Game.Beatmaps; using osu.Game.Online.Multiplayer; using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer @@ -85,6 +86,12 @@ namespace osu.Game.Tests.Visual.Multiplayer private void selectNewItem(Func beatmap) { + AddUntilStep("wait for playlist panels to load", () => + { + var queueList = this.ChildrenOfType().Single(); + return queueList.ChildrenOfType().Count() == queueList.Items.Count; + }); + AddStep("click edit button", () => { InputManager.MoveMouseTo(this.ChildrenOfType().First()); From a6e77f172d30e4f4f0126f20f0ad95197549906a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 12 Dec 2021 16:30:37 +0900 Subject: [PATCH 311/419] Add some comments --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 65190101b8..4fa21cb847 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -337,9 +337,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.UpdateWorkingBeatmap(); + // Nothing to do if the beatmap hasn't changed. if (Beatmap.Value.BeatmapInfo.MatchesOnlineID(lastBeatmap.BeatmapInfo)) return; + // The selected item is nulled during the beatmap query. During this, the working beatmap will be the dummy beatmap. + // We don't want to enter spectate mode with the dummy beatmap. if (!Beatmap.Value.BeatmapInfo.MatchesOnlineID(SelectedItem.Value?.Beatmap.Value)) return; @@ -421,9 +424,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (expectedSelectedItem == null) return; + // There's no reason to renew the selected item if its content hasn't changed. if (SelectedItem.Value?.Equals(expectedSelectedItem) == true && expectedSelectedItem.Beatmap.Value != null) return; + // Clear the selected item while the lookup is performed, so components like the ready button can enter their disabled states. SelectedItem.Value = null; if (expectedSelectedItem.Beatmap.Value == null) From 2cd2b10ce1b1fd705a5d63fe007aba821745a5d4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 13 Dec 2021 06:54:57 +0900 Subject: [PATCH 312/419] Fix results sometimes showing incorrect score position --- .../TestScenePlaylistsResultsScreen.cs | 5 ++++- .../Playlists/PlaylistsResultsScreen.cs | 7 +++++++ .../Contracted/ContractedPanelTopContent.cs | 20 +++++++++++------- osu.Game/Screens/Ranking/ScorePanel.cs | 21 +++++++++++++++---- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 4ac3b7c733..64fc4797a0 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -28,6 +28,7 @@ namespace osu.Game.Tests.Visual.Playlists public class TestScenePlaylistsResultsScreen : ScreenTestScene { private const int scores_per_result = 10; + private const int real_user_position = 200; private TestResultsScreen resultsScreen; @@ -58,6 +59,8 @@ namespace osu.Game.Tests.Visual.Playlists createResults(() => userScore); AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded); + AddAssert($"score panel position is {real_user_position}", + () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineID).ScorePosition.Value == real_user_position); } [Test] @@ -236,7 +239,7 @@ namespace osu.Game.Tests.Visual.Playlists EndedAt = userScore.Date, Passed = userScore.Passed, Rank = userScore.Rank, - Position = 200, + Position = real_user_position, MaxCombo = userScore.MaxCombo, TotalScore = userScore.TotalScore, User = userScore.User, diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs index 67727ef784..1e6722d51e 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs @@ -87,6 +87,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { var allScores = new List { userScore }; + // Other scores could have arrived between score submission and entering the results screen. Ensure the local player score position is up to date. + if (Score != null) + { + Score.Position = userScore.Position; + ScorePanelList.GetPanelForScore(Score).ScorePosition.Value = userScore.Position; + } + if (userScore.ScoresAround?.Higher != null) { allScores.AddRange(userScore.ScoresAround.Higher.Scores); diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs index 0935ee7fb2..beff509dc6 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs @@ -2,36 +2,42 @@ // 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.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Scoring; namespace osu.Game.Screens.Ranking.Contracted { public class ContractedPanelTopContent : CompositeDrawable { - private readonly ScoreInfo score; + public readonly Bindable ScorePosition = new Bindable(); - public ContractedPanelTopContent(ScoreInfo score) + private OsuSpriteText text; + + public ContractedPanelTopContent() { - this.score = score; - RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load() { - InternalChild = new OsuSpriteText + InternalChild = text = new OsuSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Y = 6, - Text = score.Position != null ? $"#{score.Position}" : string.Empty, Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold) }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ScorePosition.BindValueChanged(pos => text.Text = pos.NewValue != null ? $"#{pos.NewValue}" : string.Empty, true); + } } } diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 6ddecf8297..bc6eb9e366 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -4,6 +4,7 @@ using System; using osu.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -78,6 +79,11 @@ namespace osu.Game.Screens.Ranking public event Action StateChanged; + /// + /// The position of the score in the rankings. + /// + public readonly Bindable ScorePosition = new Bindable(); + /// /// An action to be invoked if this is clicked while in an expanded state. /// @@ -103,6 +109,8 @@ namespace osu.Game.Screens.Ranking { Score = score; displayWithFlair = isNewLocalScore; + + ScorePosition.Value = score.Position; } [BackgroundDependencyLoader] @@ -211,8 +219,8 @@ namespace osu.Game.Screens.Ranking topLayerBackground.FadeColour(expanded_top_layer_colour, RESIZE_DURATION, Easing.OutQuint); middleLayerBackground.FadeColour(expanded_middle_layer_colour, RESIZE_DURATION, Easing.OutQuint); - topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User).With(d => d.Alpha = 0)); - middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, displayWithFlair).With(d => d.Alpha = 0)); + topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User) { Alpha = 0 }); + middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, displayWithFlair) { Alpha = 0 }); // only the first expanded display should happen with flair. displayWithFlair = false; @@ -224,8 +232,13 @@ namespace osu.Game.Screens.Ranking topLayerBackground.FadeColour(contracted_top_layer_colour, RESIZE_DURATION, Easing.OutQuint); middleLayerBackground.FadeColour(contracted_middle_layer_colour, RESIZE_DURATION, Easing.OutQuint); - topLayerContentContainer.Add(topLayerContent = new ContractedPanelTopContent(Score).With(d => d.Alpha = 0)); - middleLayerContentContainer.Add(middleLayerContent = new ContractedPanelMiddleContent(Score).With(d => d.Alpha = 0)); + topLayerContentContainer.Add(topLayerContent = new ContractedPanelTopContent + { + ScorePosition = { BindTarget = ScorePosition }, + Alpha = 0 + }); + + middleLayerContentContainer.Add(middleLayerContent = new ContractedPanelMiddleContent(Score) { Alpha = 0 }); break; } From fd979a52fef9597b7e732bc8a1750723168fc520 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 13 Dec 2021 07:15:21 +0900 Subject: [PATCH 313/419] Increase score submission request timeout to 60s --- osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs | 1 + osu.Game/Online/Solo/SubmitSoloScoreRequest.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs index d5da6c401c..4ee4be6164 100644 --- a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs @@ -31,6 +31,7 @@ namespace osu.Game.Online.Rooms req.ContentType = "application/json"; req.Method = HttpMethod.Put; + req.Timeout = 60000; req.AddRaw(JsonConvert.SerializeObject(score, new JsonSerializerSettings { diff --git a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs index 25c2e5a61f..763fcf3f20 100644 --- a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs +++ b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs @@ -31,6 +31,7 @@ namespace osu.Game.Online.Solo req.ContentType = "application/json"; req.Method = HttpMethod.Put; + req.Timeout = 60000; req.AddRaw(JsonConvert.SerializeObject(score, new JsonSerializerSettings { From d0fbbf110b74761e210937b94fd2f8bffd6bb5b9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 13 Dec 2021 06:48:15 +0300 Subject: [PATCH 314/419] Expose `ScreenContainer` for access in `OsuGameDesktop` --- osu.Game/OsuGame.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 565ee7e71d..a35191613c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -103,7 +103,7 @@ namespace osu.Game private Container topMostOverlayContent; - private ScalingContainer screenContainer; + protected ScalingContainer ScreenContainer { get; private set; } protected Container ScreenOffsetContainer { get; private set; } @@ -179,7 +179,7 @@ namespace osu.Game } private void updateBlockingOverlayFade() => - screenContainer.FadeColour(visibleBlockingOverlays.Any() ? OsuColour.Gray(0.5f) : Color4.White, 500, Easing.OutQuint); + ScreenContainer.FadeColour(visibleBlockingOverlays.Any() ? OsuColour.Gray(0.5f) : Color4.White, 500, Easing.OutQuint); public void AddBlockingOverlay(OverlayContainer overlay) { @@ -698,7 +698,7 @@ namespace osu.Game RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - screenContainer = new ScalingContainer(ScalingMode.ExcludeOverlays) + ScreenContainer = new ScalingContainer(ScalingMode.ExcludeOverlays) { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -801,7 +801,7 @@ namespace osu.Game loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true); loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true); loadComponentSingleFile(wikiOverlay = new WikiOverlay(), overlayContent.Add, true); - loadComponentSingleFile(skinEditor = new SkinEditorOverlay(screenContainer), overlayContent.Add, true); + loadComponentSingleFile(skinEditor = new SkinEditorOverlay(ScreenContainer), overlayContent.Add, true); loadComponentSingleFile(new LoginOverlay { From d92f5039cd78edf204e9b40ede9f72869191019f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 13 Dec 2021 06:48:40 +0300 Subject: [PATCH 315/419] Reorder version overlay to display behind game-wide overlays --- osu.Desktop/OsuGameDesktop.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 645ea66654..b234207848 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -70,7 +70,9 @@ namespace osu.Desktop if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath)) return stableInstallPath; } - catch { } + catch + { + } } stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); @@ -113,7 +115,7 @@ namespace osu.Desktop base.LoadComplete(); if (!noVersionOverlay) - LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add); + LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, ScreenContainer.Add); LoadComponentAsync(new DiscordRichPresence(), Add); From c097dc80488ff45357b33eb907c12bb8a34629db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Dec 2021 13:39:32 +0900 Subject: [PATCH 316/419] Add note about reasoning behind `MultiplayerClient.GetAPIBeatmap` call --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index a877c6aca9..e1f7ea5e92 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -166,6 +166,8 @@ namespace osu.Game.Screens.OnlinePlay IBeatmapInfo foundBeatmap; if (multiplayerClient != null) + // This call can eventually go away (and use the else case below). + // Currently required only due to the method being overridden to provide special behaviour in tests. foundBeatmap = await multiplayerClient.GetAPIBeatmap(Item.BeatmapID).ConfigureAwait(false); else foundBeatmap = await beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ConfigureAwait(false); From cac684c0448d69f5a3d427c7b0a375e5266dd93a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Dec 2021 14:46:49 +0900 Subject: [PATCH 317/419] Improve appearance of player-wide background after failing with low background dim --- osu.Game/Screens/Play/FailAnimation.cs | 10 ++++++++++ osu.Game/Screens/Play/Player.cs | 2 ++ 2 files changed, 12 insertions(+) diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index 193e1e4129..aa777f18a8 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Game.Rulesets.UI; using System; using System.Collections.Generic; +using JetBrains.Annotations; using ManagedBass.Fx; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; @@ -18,6 +19,7 @@ using osu.Framework.Utils; using osu.Game.Audio.Effects; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osuTK; using osuTK.Graphics; @@ -58,6 +60,12 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both, }; + /// + /// The player screen background, used to adjust appearance on failing. + /// + [CanBeNull] + public BackgroundScreen Background { private get; set; } + public FailAnimation(DrawableRuleset drawableRuleset) { this.drawableRuleset = drawableRuleset; @@ -136,6 +144,8 @@ namespace osu.Game.Screens.Play Content.ScaleTo(0.85f, duration, Easing.OutQuart); Content.RotateTo(1, duration, Easing.OutQuart); Content.FadeColour(Color4.Gray, duration); + + Background?.FadeColour(OsuColour.Gray(0.3f), 60); } public void RemoveFilters(bool resetTrackFrequency = true) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e2896c174f..745e1f9e7c 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -921,6 +921,8 @@ namespace osu.Game.Screens.Play b.IsBreakTime.BindTo(breakTracker.IsBreakTime); b.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); + + failAnimationLayer.Background = b; }); HUDOverlay.IsBreakTime.BindTo(breakTracker.IsBreakTime); From 4cac87e93323a46752fb2353ed41488ba0f332b7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Dec 2021 15:26:32 +0900 Subject: [PATCH 318/419] Add test coverage showing that `KeyBindingStore` won't remove excess key bindings --- .../Database/TestRealmKeyBindingStore.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index 860828ae81..f05d9ab3dc 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -52,6 +52,45 @@ namespace osu.Game.Tests.Database Assert.That(queryCount(GlobalAction.Select), Is.EqualTo(2)); } + [Test] + public void TestDefaultsPopulationRemovesExcess() + { + Assert.That(queryCount(), Is.EqualTo(0)); + + KeyBindingContainer testContainer = new TestKeyBindingContainer(); + + // Add some excess bindings for an action which only supports 1. + using (var realm = realmContextFactory.CreateContext()) + using (var transaction = realm.BeginWrite()) + { + realm.Add(new RealmKeyBinding + { + Action = GlobalAction.Back, + KeyCombination = new KeyCombination(InputKey.A) + }); + + realm.Add(new RealmKeyBinding + { + Action = GlobalAction.Back, + KeyCombination = new KeyCombination(InputKey.S) + }); + + realm.Add(new RealmKeyBinding + { + Action = GlobalAction.Back, + KeyCombination = new KeyCombination(InputKey.D) + }); + + transaction.Commit(); + } + + Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(3)); + + keyBindingStore.Register(testContainer, Enumerable.Empty()); + + Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(1)); + } + private int queryCount(GlobalAction? match = null) { using (var realm = realmContextFactory.CreateContext()) From 7318ff3f98cee2e689d1f20ea736a8ec6b4fdd3d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Dec 2021 15:26:49 +0900 Subject: [PATCH 319/419] Refactor `KeyBindingStore` to clean up any excess bindings for individual actions While this isn't strictly required (outside of rulesets, potentially), I think it's best that we keep these counts in a sane state. Right now, it will remove any excess. Arguably, in the case an entry is found with too many, we should wipe all entries and re-populate the defaults. Interested in opinions on that before merging. See https://github.com/ppy/osu/discussions/15925 for an example where wiping may be the more appropriate behaviour. --- osu.Game/Input/RealmKeyBindingStore.cs | 39 ++++++++++++++++++-------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 3bdb0a180d..cb51797685 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -81,20 +81,37 @@ namespace osu.Game.Input // compare counts in database vs defaults for each action type. foreach (var defaultsForAction in defaults.GroupBy(k => k.Action)) { - // avoid performing redundant queries when the database is empty and needs to be re-filled. - int existingCount = existingBindings.Count(k => k.RulesetName == rulesetName && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key); + IEnumerable existing = existingBindings.Where(k => + k.RulesetName == rulesetName + && k.Variant == variant + && k.ActionInt == (int)defaultsForAction.Key); - if (defaultsForAction.Count() <= existingCount) - continue; + int defaultsCount = defaultsForAction.Count(); + int existingCount = existing.Count(); - // insert any defaults which are missing. - realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding + if (defaultsCount > existingCount) { - KeyCombinationString = k.KeyCombination.ToString(), - ActionInt = (int)k.Action, - RulesetName = rulesetName, - Variant = variant - })); + // insert any defaults which are missing. + realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding + { + KeyCombinationString = k.KeyCombination.ToString(), + ActionInt = (int)k.Action, + RulesetName = rulesetName, + Variant = variant + })); + } + else if (defaultsCount < existingCount) + { + // generally this shouldn't happen, but if the user has more key bindings for an action than we expect, + // remove the last entries until the count matches for sanity. + foreach (var k in existing.TakeLast(existingCount - defaultsCount).ToArray()) + { + realm.Remove(k); + + // Remove from the local flattened/cached list so future lookups don't query now deleted rows. + existingBindings.Remove(k); + } + } } } From 70045494babd97eec564f624e3a5a38a1019ebdc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 13 Dec 2021 16:09:53 +0900 Subject: [PATCH 320/419] Re-simplify code by removing BeatmapManager event instead --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 36 ++++--------------- .../Multiplayer/MultiplayerMatchSubScreen.cs | 24 ++----------- 2 files changed, 9 insertions(+), 51 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 6b395058fa..a560d85b7d 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; -using osu.Game.Online; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -68,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Match protected OnlinePlayScreen ParentScreen { get; private set; } [Cached] - private OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker { get; set; } + private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); protected IBindable BeatmapAvailability => beatmapAvailabilityTracker.Availability; @@ -91,11 +90,6 @@ namespace osu.Game.Screens.OnlinePlay.Match Padding = new MarginPadding { Top = Header.HEIGHT }; - beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker - { - SelectedItem = { BindTarget = SelectedItem } - }; - RoomId.BindTo(room.RoomID); } @@ -248,10 +242,10 @@ namespace osu.Game.Screens.OnlinePlay.Match }, true); SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged)); - - beatmapManager.ItemUpdated += beatmapUpdated; - UserMods.BindValueChanged(_ => Scheduler.AddOnce(UpdateMods)); + + beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); + beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateWorkingBeatmap()); } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -303,7 +297,7 @@ namespace osu.Game.Screens.OnlinePlay.Match public override void OnResuming(IScreen last) { base.OnResuming(last); - UpdateWorkingBeatmap(); + updateWorkingBeatmap(); beginHandlingTrack(); Scheduler.AddOnce(UpdateMods); } @@ -346,7 +340,7 @@ namespace osu.Game.Screens.OnlinePlay.Match private void selectedItemChanged() { - UpdateWorkingBeatmap(); + updateWorkingBeatmap(); var selected = SelectedItem.Value; @@ -375,16 +369,8 @@ namespace osu.Game.Screens.OnlinePlay.Match } } - private void beatmapUpdated(BeatmapSetInfo set) => Schedule(UpdateWorkingBeatmap); - - protected virtual void UpdateWorkingBeatmap() + private void updateWorkingBeatmap() { - if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable) - { - Beatmap.Value = beatmapManager.GetWorkingBeatmap(null); - return; - } - var beatmap = SelectedItem.Value?.Beatmap.Value; // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info @@ -450,14 +436,6 @@ namespace osu.Game.Screens.OnlinePlay.Match /// The room to change the settings of. protected abstract RoomSettingsOverlay CreateRoomSettingsOverlay(Room room); - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (beatmapManager != null) - beatmapManager.ItemUpdated -= beatmapUpdated; - } - public class UserModSelectButton : PurpleTriangleButton { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 4fa21cb847..6895608c8e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -15,7 +15,6 @@ using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; -using osu.Game.Extensions; using osu.Game.Online; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -327,27 +326,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.LocalUser?.State == MultiplayerUserState.Ready) client.ChangeState(MultiplayerUserState.Idle); } - - UpdateWorkingBeatmap(); - } - - protected override void UpdateWorkingBeatmap() - { - var lastBeatmap = Beatmap.Value; - - base.UpdateWorkingBeatmap(); - - // Nothing to do if the beatmap hasn't changed. - if (Beatmap.Value.BeatmapInfo.MatchesOnlineID(lastBeatmap.BeatmapInfo)) - return; - - // The selected item is nulled during the beatmap query. During this, the working beatmap will be the dummy beatmap. - // We don't want to enter spectate mode with the dummy beatmap. - if (!Beatmap.Value.BeatmapInfo.MatchesOnlineID(SelectedItem.Value?.Beatmap.Value)) - return; - - if (client.LocalUser?.State == MultiplayerUserState.Spectating - && (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing)) + else if (client.LocalUser?.State == MultiplayerUserState.Spectating + && (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing)) { onLoadRequested(); } From b0d14526eac084234a7b4c2e1d30f63a36262f35 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Dec 2021 16:34:48 +0900 Subject: [PATCH 321/419] Move test `ScoreInfo` creation to `TestResources` The main goal here is to remove the inheritance, since realm doesn't like that. Unfortunate that we can't use object initialisers in a few of these places, but no real way around that. --- .../Formats/LegacyScoreDecoderTest.cs | 2 +- osu.Game.Tests/Resources/TestResources.cs | 67 +++++++++++++++++++ .../TestScenePlaylistsResultsScreen.cs | 13 +++- .../TestSceneContractedPanelMiddleContent.cs | 5 +- .../TestSceneExpandedPanelMiddleContent.cs | 28 +++----- .../TestSceneExpandedPanelTopContent.cs | 4 +- .../Visual/Ranking/TestSceneResultsScreen.cs | 20 +++--- .../Visual/Ranking/TestSceneScorePanel.cs | 42 +++++++++--- .../Visual/Ranking/TestSceneScorePanelList.cs | 47 ++++++++----- .../Ranking/TestSceneStatisticsPanel.cs | 15 ++--- osu.Game/Tests/TestScoreInfo.cs | 66 ------------------ 11 files changed, 170 insertions(+), 139 deletions(-) delete mode 100644 osu.Game/Tests/TestScoreInfo.cs diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index a73ae9dcdb..81d89359e0 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Beatmaps.Formats public void TestCultureInvariance() { var ruleset = new OsuRuleset().RulesetInfo; - var scoreInfo = new TestScoreInfo(ruleset); + var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); var beatmap = new TestBeatmap(ruleset); var score = new Score { diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index 440d5e701f..7535f6769f 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Threading; using NUnit.Framework; @@ -12,8 +13,12 @@ using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; namespace osu.Game.Tests.Resources { @@ -137,5 +142,67 @@ namespace osu.Game.Tests.Resources } } } + + /// + /// Create a test score model. + /// + /// The ruleset for which the score was set against. + /// Whether to include an excessive number of mods. If false, only two will be added. + /// + public static ScoreInfo CreateTestScoreInfo(RulesetInfo ruleset = null, bool excessMods = false) => + CreateTestScoreInfo(CreateTestBeatmapSetInfo(1, new[] { ruleset ?? new OsuRuleset().RulesetInfo }).Beatmaps.First(), excessMods); + + /// + /// Create a test score model. + /// + /// The beatmap for which the score was set against. + /// Whether to include an excessive number of mods. If false, only two will be added. + /// + public static ScoreInfo CreateTestScoreInfo(BeatmapInfo beatmap, bool excessMods = false) => new ScoreInfo + { + User = new APIUser + { + Id = 2, + Username = "peppy", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }, + BeatmapInfo = beatmap, + Ruleset = beatmap.Ruleset, + RulesetID = beatmap.Ruleset.ID ?? 0, + Mods = excessMods + ? beatmap.Ruleset.CreateInstance().CreateAllMods().ToArray() + : new Mod[] { new TestModHardRock(), new TestModDoubleTime() }, + TotalScore = 2845370, + Accuracy = 0.95, + MaxCombo = 999, + Position = 1, + Rank = ScoreRank.S, + Date = DateTimeOffset.Now, + Statistics = new Dictionary + { + [HitResult.Miss] = 1, + [HitResult.Meh] = 50, + [HitResult.Ok] = 100, + [HitResult.Good] = 200, + [HitResult.Great] = 300, + [HitResult.Perfect] = 320, + [HitResult.SmallTickHit] = 50, + [HitResult.SmallTickMiss] = 25, + [HitResult.LargeTickHit] = 100, + [HitResult.LargeTickMiss] = 50, + [HitResult.SmallBonus] = 10, + [HitResult.SmallBonus] = 50 + }, + }; + + private class TestModHardRock : ModHardRock + { + public override double ScoreMultiplier => 1; + } + + private class TestModDoubleTime : ModDoubleTime + { + public override double ScoreMultiplier => 1; + } } } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 64fc4797a0..faa0ce2bdc 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -22,6 +22,7 @@ using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Ranking; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.Playlists { @@ -52,7 +53,9 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => { - userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineID = currentScoreId++ }; + userScore = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); + userScore.OnlineID = currentScoreId++; + bindHandler(userScore: userScore); }); @@ -78,7 +81,9 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => { - userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineID = currentScoreId++ }; + userScore = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); + userScore.OnlineID = currentScoreId++; + bindHandler(true, userScore); }); @@ -127,7 +132,9 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => { - userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineID = currentScoreId++ }; + userScore = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); + userScore.OnlineID = currentScoreId++; + bindHandler(userScore: userScore); }); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs index f246560c82..04ddc5d7f0 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking.Contracted; +using osu.Game.Tests.Resources; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -22,13 +23,13 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestShowPanel() { - AddStep("show example score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), new TestScoreInfo(new OsuRuleset().RulesetInfo))); + AddStep("show example score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo))); } [Test] public void TestExcessMods() { - AddStep("show excess mods score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), new TestScoreInfo(new OsuRuleset().RulesetInfo, true))); + AddStep("show excess mods score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo, true))); } private void showPanel(WorkingBeatmap workingBeatmap, ScoreInfo score) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 9983993d9c..1f34606fcb 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -20,6 +20,7 @@ using osu.Game.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking.Expanded; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -34,10 +35,7 @@ namespace osu.Game.Tests.Visual.Ranking { var author = new APIUser { Username = "mapper_name" }; - AddStep("show example score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo) - { - BeatmapInfo = createTestBeatmap(author) - })); + AddStep("show example score", () => showPanel(TestResources.CreateTestScoreInfo(createTestBeatmap(author)))); } [Test] @@ -45,10 +43,7 @@ namespace osu.Game.Tests.Visual.Ranking { var author = new APIUser { Username = "mapper_name" }; - AddStep("show excess mods score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo, true) - { - BeatmapInfo = createTestBeatmap(author) - })); + AddStep("show excess mods score", () => showPanel(TestResources.CreateTestScoreInfo(createTestBeatmap(author), true))); AddAssert("mapper name present", () => this.ChildrenOfType().Any(spriteText => spriteText.Current.Value == "mapper_name")); } @@ -56,10 +51,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestMapWithUnknownMapper() { - AddStep("show example score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo) - { - BeatmapInfo = createTestBeatmap(new APIUser()) - })); + AddStep("show example score", () => showPanel(TestResources.CreateTestScoreInfo(createTestBeatmap(new APIUser())))); AddAssert("mapped by text not present", () => this.ChildrenOfType().All(spriteText => !containsAny(spriteText.Text.ToString(), "mapped", "by"))); @@ -77,12 +69,12 @@ namespace osu.Game.Tests.Visual.Ranking var mods = new Mod[] { ruleset.GetAutoplayMod() }; var beatmap = createTestBeatmap(new APIUser()); - showPanel(new TestScoreInfo(ruleset.RulesetInfo) - { - Mods = mods, - BeatmapInfo = beatmap, - Date = default, - }); + var score = TestResources.CreateTestScoreInfo(beatmap); + + score.Mods = mods; + score.Date = default; + + showPanel(score); }); AddAssert("play time not displayed", () => !this.ChildrenOfType().Any()); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs index a32bcbe7f0..a2fa142896 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs @@ -5,8 +5,8 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Osu; using osu.Game.Screens.Ranking.Expanded; +using osu.Game.Tests.Resources; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#444"), }, - new ExpandedPanelTopContent(new TestScoreInfo(new OsuRuleset().RulesetInfo).User), + new ExpandedPanelTopContent(TestResources.CreateTestScoreInfo().User), } }; } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 94700bac6a..809f513a83 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -21,6 +21,7 @@ using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking.Statistics; +using osu.Game.Tests.Resources; using osuTK; using osuTK.Input; @@ -72,11 +73,10 @@ namespace osu.Game.Tests.Visual.Ranking { TestResultsScreen screen = null; - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) - { - Accuracy = accuracy, - Rank = rank - }; + var score = TestResources.CreateTestScoreInfo(); + + score.Accuracy = accuracy; + score.Rank = rank; AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen(score))); AddUntilStep("wait for loaded", () => screen.IsLoaded); @@ -204,7 +204,7 @@ namespace osu.Game.Tests.Visual.Ranking { DelayedFetchResultsScreen screen = null; - AddStep("load results", () => Child = new TestResultsContainer(screen = new DelayedFetchResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo), 3000))); + AddStep("load results", () => Child = new TestResultsContainer(screen = new DelayedFetchResultsScreen(TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo), 3000))); AddUntilStep("wait for loaded", () => screen.IsLoaded); AddStep("click expanded panel", () => { @@ -237,9 +237,9 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("download button is enabled", () => screen.ChildrenOfType().Last().Enabled.Value); } - private TestResultsScreen createResultsScreen(ScoreInfo score = null) => new TestResultsScreen(score ?? new TestScoreInfo(new OsuRuleset().RulesetInfo)); + private TestResultsScreen createResultsScreen(ScoreInfo score = null) => new TestResultsScreen(score ?? TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo)); - private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo)); private class TestResultsContainer : Container { @@ -282,7 +282,7 @@ namespace osu.Game.Tests.Visual.Ranking for (int i = 0; i < 20; i++) { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); + var score = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); score.TotalScore += 10 - i; score.Hash = $"test{i}"; scores.Add(score); @@ -316,7 +316,7 @@ namespace osu.Game.Tests.Visual.Ranking for (int i = 0; i < 20; i++) { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); + var score = TestResources.CreateTestScoreInfo(); score.TotalScore += 10 - i; scores.Add(score); } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs index 5af55e99f8..5dbeefd390 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs @@ -3,10 +3,10 @@ using NUnit.Framework; using osu.Framework.Graphics; -using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking; +using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.Ranking { @@ -17,7 +17,9 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestDRank() { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.5, Rank = ScoreRank.D }; + var score = TestResources.CreateTestScoreInfo(); + score.Accuracy = 0.5; + score.Rank = ScoreRank.D; addPanelStep(score); } @@ -25,7 +27,9 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestCRank() { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.75, Rank = ScoreRank.C }; + var score = TestResources.CreateTestScoreInfo(); + score.Accuracy = 0.75; + score.Rank = ScoreRank.C; addPanelStep(score); } @@ -33,7 +37,9 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestBRank() { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.85, Rank = ScoreRank.B }; + var score = TestResources.CreateTestScoreInfo(); + score.Accuracy = 0.85; + score.Rank = ScoreRank.B; addPanelStep(score); } @@ -41,7 +47,9 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestARank() { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A }; + var score = TestResources.CreateTestScoreInfo(); + score.Accuracy = 0.925; + score.Rank = ScoreRank.A; addPanelStep(score); } @@ -49,7 +57,9 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestSRank() { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.975, Rank = ScoreRank.S }; + var score = TestResources.CreateTestScoreInfo(); + score.Accuracy = 0.975; + score.Rank = ScoreRank.S; addPanelStep(score); } @@ -57,7 +67,9 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAlmostSSRank() { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.9999, Rank = ScoreRank.S }; + var score = TestResources.CreateTestScoreInfo(); + score.Accuracy = 0.9999; + score.Rank = ScoreRank.S; addPanelStep(score); } @@ -65,7 +77,9 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestSSRank() { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 1, Rank = ScoreRank.X }; + var score = TestResources.CreateTestScoreInfo(); + score.Accuracy = 1; + score.Rank = ScoreRank.X; addPanelStep(score); } @@ -73,7 +87,9 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAllHitResults() { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Statistics = { [HitResult.Perfect] = 350, [HitResult.Ok] = 200 } }; + var score = TestResources.CreateTestScoreInfo(); + score.Statistics[HitResult.Perfect] = 350; + score.Statistics[HitResult.Ok] = 200; addPanelStep(score); } @@ -81,7 +97,9 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestContractedPanel() { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A }; + var score = TestResources.CreateTestScoreInfo(); + score.Accuracy = 0.925; + score.Rank = ScoreRank.A; addPanelStep(score, PanelState.Contracted); } @@ -89,7 +107,9 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestExpandAndContract() { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A }; + var score = TestResources.CreateTestScoreInfo(); + score.Accuracy = 0.925; + score.Rank = ScoreRank.A; addPanelStep(score, PanelState.Contracted); AddWaitStep("wait for transition", 10); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs index 2f9652d354..f963be5d81 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs @@ -10,6 +10,7 @@ using osu.Framework.Utils; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens.Ranking; +using osu.Game.Tests.Resources; using osuTK.Input; namespace osu.Game.Tests.Visual.Ranking @@ -29,14 +30,14 @@ namespace osu.Game.Tests.Visual.Ranking { createListStep(() => new ScorePanelList { - SelectedScore = { Value = new TestScoreInfo(new OsuRuleset().RulesetInfo) } + SelectedScore = { Value = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo) } }); } [Test] public void TestAddPanelAfterSelectingScore() { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); + var score = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); createListStep(() => new ScorePanelList { @@ -52,7 +53,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAddPanelBeforeSelectingScore() { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); + var score = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); createListStep(() => new ScorePanelList()); @@ -75,7 +76,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("add many scores", () => { for (int i = 0; i < 20; i++) - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + list.AddScore(TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo)); }); assertFirstPanelCentred(); @@ -84,7 +85,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAddManyScoresAfterExpandedPanel() { - var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); + var initialScore = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); createListStep(() => new ScorePanelList()); @@ -97,7 +98,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("add many scores", () => { for (int i = 0; i < 20; i++) - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore - i - 1 }); + list.AddScore(createScoreForTotalScore(initialScore.TotalScore - i - 1)); }); assertScoreState(initialScore, true); @@ -107,7 +108,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAddManyScoresBeforeExpandedPanel() { - var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); + var initialScore = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); createListStep(() => new ScorePanelList()); @@ -120,7 +121,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("add scores", () => { for (int i = 0; i < 20; i++) - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore + i + 1 }); + list.AddScore(createScoreForTotalScore(initialScore.TotalScore + i + 1)); }); assertScoreState(initialScore, true); @@ -130,7 +131,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAddManyPanelsOnBothSidesOfExpandedPanel() { - var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); + var initialScore = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); createListStep(() => new ScorePanelList()); @@ -143,10 +144,10 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("add scores after", () => { for (int i = 0; i < 20; i++) - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore - i - 1 }); + list.AddScore(createScoreForTotalScore(initialScore.TotalScore - i - 1)); for (int i = 0; i < 20; i++) - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore + i + 1 }); + list.AddScore(createScoreForTotalScore(initialScore.TotalScore + i + 1)); }); assertScoreState(initialScore, true); @@ -156,8 +157,8 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestSelectMultipleScores() { - var firstScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); - var secondScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); + var firstScore = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); + var secondScore = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); firstScore.UserString = "A"; secondScore.UserString = "B"; @@ -190,7 +191,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAddScoreImmediately() { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); + var score = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); createListStep(() => { @@ -206,9 +207,14 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestKeyboardNavigation() { - var lowestScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { MaxCombo = 100 }; - var middleScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { MaxCombo = 200 }; - var highestScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { MaxCombo = 300 }; + var lowestScore = TestResources.CreateTestScoreInfo(); + lowestScore.MaxCombo = 100; + + var middleScore = TestResources.CreateTestScoreInfo(); + middleScore.MaxCombo = 200; + + var highestScore = TestResources.CreateTestScoreInfo(); + highestScore.MaxCombo = 300; createListStep(() => new ScorePanelList()); @@ -270,6 +276,13 @@ namespace osu.Game.Tests.Visual.Ranking assertExpandedPanelCentred(); } + private ScoreInfo createScoreForTotalScore(long totalScore) + { + var score = TestResources.CreateTestScoreInfo(); + score.TotalScore = totalScore; + return score; + } + private void createListStep(Func creationFunc) { AddStep("create list", () => Child = list = creationFunc().With(d => diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index d91aec753c..ebea523b9e 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -11,6 +11,7 @@ using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Tests.Resources; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -20,10 +21,8 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestScoreWithTimeStatistics() { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) - { - HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents() - }; + var score = TestResources.CreateTestScoreInfo(); + score.HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); loadPanel(score); } @@ -31,10 +30,8 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestScoreWithPositionStatistics() { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) - { - HitEvents = createPositionDistributedHitEvents() - }; + var score = TestResources.CreateTestScoreInfo(); + score.HitEvents = createPositionDistributedHitEvents(); loadPanel(score); } @@ -42,7 +39,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestScoreWithoutStatistics() { - loadPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + loadPanel(TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo)); } [Test] diff --git a/osu.Game/Tests/TestScoreInfo.cs b/osu.Game/Tests/TestScoreInfo.cs deleted file mode 100644 index a53cb0ae78..0000000000 --- a/osu.Game/Tests/TestScoreInfo.cs +++ /dev/null @@ -1,66 +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.Linq; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; -using osu.Game.Tests.Beatmaps; - -namespace osu.Game.Tests -{ - public class TestScoreInfo : ScoreInfo - { - public TestScoreInfo(RulesetInfo ruleset, bool excessMods = false) - { - User = new APIUser - { - Id = 2, - Username = "peppy", - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", - }; - - BeatmapInfo = new TestBeatmap(ruleset).BeatmapInfo; - Ruleset = ruleset; - RulesetID = ruleset.ID ?? 0; - - Mods = excessMods - ? ruleset.CreateInstance().CreateAllMods().ToArray() - : new Mod[] { new TestModHardRock(), new TestModDoubleTime() }; - - TotalScore = 2845370; - Accuracy = 0.95; - MaxCombo = 999; - Rank = ScoreRank.S; - Date = DateTimeOffset.Now; - - Statistics[HitResult.Miss] = 1; - Statistics[HitResult.Meh] = 50; - Statistics[HitResult.Ok] = 100; - Statistics[HitResult.Good] = 200; - Statistics[HitResult.Great] = 300; - Statistics[HitResult.Perfect] = 320; - Statistics[HitResult.SmallTickHit] = 50; - Statistics[HitResult.SmallTickMiss] = 25; - Statistics[HitResult.LargeTickHit] = 100; - Statistics[HitResult.LargeTickMiss] = 50; - Statistics[HitResult.SmallBonus] = 10; - Statistics[HitResult.SmallBonus] = 50; - - Position = 1; - } - - private class TestModHardRock : ModHardRock - { - public override double ScoreMultiplier => 1; - } - - private class TestModDoubleTime : ModDoubleTime - { - public override double ScoreMultiplier => 1; - } - } -} From 99ac71c1fe9b109c064b2deac88aadd10b290544 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Dec 2021 16:37:20 +0900 Subject: [PATCH 322/419] Simplify usages where the ruleset being used is osu! ruleset --- .../TestScenePlaylistsResultsScreen.cs | 6 +++--- .../TestSceneContractedPanelMiddleContent.cs | 4 ++-- .../Visual/Ranking/TestSceneResultsScreen.cs | 9 ++++---- .../Visual/Ranking/TestSceneScorePanelList.cs | 21 +++++++++---------- .../Ranking/TestSceneStatisticsPanel.cs | 3 +-- 5 files changed, 20 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index faa0ce2bdc..25ca1299ef 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => { - userScore = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); + userScore = TestResources.CreateTestScoreInfo(); userScore.OnlineID = currentScoreId++; bindHandler(userScore: userScore); @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => { - userScore = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); + userScore = TestResources.CreateTestScoreInfo(); userScore.OnlineID = currentScoreId++; bindHandler(true, userScore); @@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => { - userScore = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); + userScore = TestResources.CreateTestScoreInfo(); userScore.OnlineID = currentScoreId++; bindHandler(userScore: userScore); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs index 04ddc5d7f0..a84a7b3993 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs @@ -23,13 +23,13 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestShowPanel() { - AddStep("show example score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo))); + AddStep("show example score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), TestResources.CreateTestScoreInfo())); } [Test] public void TestExcessMods() { - AddStep("show excess mods score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo, true))); + AddStep("show excess mods score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), TestResources.CreateTestScoreInfo(excessMods: true))); } private void showPanel(WorkingBeatmap workingBeatmap, ScoreInfo score) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 809f513a83..d0bd5a6e66 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -15,7 +15,6 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; -using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; @@ -204,7 +203,7 @@ namespace osu.Game.Tests.Visual.Ranking { DelayedFetchResultsScreen screen = null; - AddStep("load results", () => Child = new TestResultsContainer(screen = new DelayedFetchResultsScreen(TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo), 3000))); + AddStep("load results", () => Child = new TestResultsContainer(screen = new DelayedFetchResultsScreen(TestResources.CreateTestScoreInfo(), 3000))); AddUntilStep("wait for loaded", () => screen.IsLoaded); AddStep("click expanded panel", () => { @@ -237,9 +236,9 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("download button is enabled", () => screen.ChildrenOfType().Last().Enabled.Value); } - private TestResultsScreen createResultsScreen(ScoreInfo score = null) => new TestResultsScreen(score ?? TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo)); + private TestResultsScreen createResultsScreen(ScoreInfo score = null) => new TestResultsScreen(score ?? TestResources.CreateTestScoreInfo()); - private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo)); + private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(TestResources.CreateTestScoreInfo()); private class TestResultsContainer : Container { @@ -282,7 +281,7 @@ namespace osu.Game.Tests.Visual.Ranking for (int i = 0; i < 20; i++) { - var score = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); + var score = TestResources.CreateTestScoreInfo(); score.TotalScore += 10 - i; score.Hash = $"test{i}"; scores.Add(score); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs index f963be5d81..f5ad352b9c 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs @@ -7,7 +7,6 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Framework.Utils; -using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Tests.Resources; @@ -30,14 +29,14 @@ namespace osu.Game.Tests.Visual.Ranking { createListStep(() => new ScorePanelList { - SelectedScore = { Value = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo) } + SelectedScore = { Value = TestResources.CreateTestScoreInfo() } }); } [Test] public void TestAddPanelAfterSelectingScore() { - var score = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); + var score = TestResources.CreateTestScoreInfo(); createListStep(() => new ScorePanelList { @@ -53,7 +52,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAddPanelBeforeSelectingScore() { - var score = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); + var score = TestResources.CreateTestScoreInfo(); createListStep(() => new ScorePanelList()); @@ -76,7 +75,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("add many scores", () => { for (int i = 0; i < 20; i++) - list.AddScore(TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo)); + list.AddScore(TestResources.CreateTestScoreInfo()); }); assertFirstPanelCentred(); @@ -85,7 +84,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAddManyScoresAfterExpandedPanel() { - var initialScore = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); + var initialScore = TestResources.CreateTestScoreInfo(); createListStep(() => new ScorePanelList()); @@ -108,7 +107,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAddManyScoresBeforeExpandedPanel() { - var initialScore = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); + var initialScore = TestResources.CreateTestScoreInfo(); createListStep(() => new ScorePanelList()); @@ -131,7 +130,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAddManyPanelsOnBothSidesOfExpandedPanel() { - var initialScore = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); + var initialScore = TestResources.CreateTestScoreInfo(); createListStep(() => new ScorePanelList()); @@ -157,8 +156,8 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestSelectMultipleScores() { - var firstScore = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); - var secondScore = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); + var firstScore = TestResources.CreateTestScoreInfo(); + var secondScore = TestResources.CreateTestScoreInfo(); firstScore.UserString = "A"; secondScore.UserString = "B"; @@ -191,7 +190,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAddScoreImmediately() { - var score = TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo); + var score = TestResources.CreateTestScoreInfo(); createListStep(() => { diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index ebea523b9e..f64b7b2b65 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Rulesets.Osu.Objects; @@ -39,7 +38,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestScoreWithoutStatistics() { - loadPanel(TestResources.CreateTestScoreInfo(new OsuRuleset().RulesetInfo)); + loadPanel(TestResources.CreateTestScoreInfo()); } [Test] From 654b47c7ecd67272038672a9f468059f667b0247 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Dec 2021 16:41:29 +0900 Subject: [PATCH 323/419] Move "excess mods" test behaviour to local usages There were only two of these, so it doesn't make sense to add extra complexity in the test resources class. --- osu.Game.Tests/Resources/TestResources.cs | 12 ++++-------- .../Ranking/TestSceneContractedPanelMiddleContent.cs | 8 +++++++- .../Ranking/TestSceneExpandedPanelMiddleContent.cs | 10 ++++++++-- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index 7535f6769f..445394fc77 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -147,18 +147,16 @@ namespace osu.Game.Tests.Resources /// Create a test score model. /// /// The ruleset for which the score was set against. - /// Whether to include an excessive number of mods. If false, only two will be added. /// - public static ScoreInfo CreateTestScoreInfo(RulesetInfo ruleset = null, bool excessMods = false) => - CreateTestScoreInfo(CreateTestBeatmapSetInfo(1, new[] { ruleset ?? new OsuRuleset().RulesetInfo }).Beatmaps.First(), excessMods); + public static ScoreInfo CreateTestScoreInfo(RulesetInfo ruleset = null) => + CreateTestScoreInfo(CreateTestBeatmapSetInfo(1, new[] { ruleset ?? new OsuRuleset().RulesetInfo }).Beatmaps.First()); /// /// Create a test score model. /// /// The beatmap for which the score was set against. - /// Whether to include an excessive number of mods. If false, only two will be added. /// - public static ScoreInfo CreateTestScoreInfo(BeatmapInfo beatmap, bool excessMods = false) => new ScoreInfo + public static ScoreInfo CreateTestScoreInfo(BeatmapInfo beatmap) => new ScoreInfo { User = new APIUser { @@ -169,9 +167,7 @@ namespace osu.Game.Tests.Resources BeatmapInfo = beatmap, Ruleset = beatmap.Ruleset, RulesetID = beatmap.Ruleset.ID ?? 0, - Mods = excessMods - ? beatmap.Ruleset.CreateInstance().CreateAllMods().ToArray() - : new Mod[] { new TestModHardRock(), new TestModDoubleTime() }, + Mods = new Mod[] { new TestModHardRock(), new TestModDoubleTime() }, TotalScore = 2845370, Accuracy = 0.95, MaxCombo = 999, diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs index a84a7b3993..85306b9354 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.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.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -29,7 +30,12 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestExcessMods() { - AddStep("show excess mods score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), TestResources.CreateTestScoreInfo(excessMods: true))); + AddStep("show excess mods score", () => + { + var score = TestResources.CreateTestScoreInfo(); + score.Mods = score.BeatmapInfo.Ruleset.CreateInstance().CreateAllMods().ToArray(); + showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), score); + }); } private void showPanel(WorkingBeatmap workingBeatmap, ScoreInfo score) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 1f34606fcb..2cb4fb6b6b 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -41,9 +41,15 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestExcessMods() { - var author = new APIUser { Username = "mapper_name" }; + AddStep("show excess mods score", () => + { + var author = new APIUser { Username = "mapper_name" }; - AddStep("show excess mods score", () => showPanel(TestResources.CreateTestScoreInfo(createTestBeatmap(author), true))); + var score = TestResources.CreateTestScoreInfo(createTestBeatmap(author)); + score.Mods = score.BeatmapInfo.Ruleset.CreateInstance().CreateAllMods().ToArray(); + + showPanel(score); + }); AddAssert("mapper name present", () => this.ChildrenOfType().Any(spriteText => spriteText.Current.Value == "mapper_name")); } From 309290a3c9b92e40d88328720b23e544de7c892b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Dec 2021 16:50:29 +0900 Subject: [PATCH 324/419] Use new method in more places that can benefit from it --- .../Online/TestAPIModJsonSerialization.cs | 3 +-- .../Background/TestSceneUserDimBackgrounds.cs | 9 +-------- .../Visual/Gameplay/TestSceneFailJudgement.cs | 1 + .../TestSceneMultiplayerResults.cs | 19 ++----------------- .../TestSceneMultiplayerTeamResults.cs | 19 ++----------------- .../SongSelect/TestScenePlaySongSelect.cs | 7 +------ 6 files changed, 8 insertions(+), 50 deletions(-) diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs index 8378b33b3d..8b8954a1d0 100644 --- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Online [Test] public void TestDeserialiseSubmittableScoreWithEmptyMods() { - var score = new SubmittableScore(new ScoreInfo { Ruleset = new OsuRuleset().RulesetInfo }); + var score = new SubmittableScore(new ScoreInfo()); var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(score)); @@ -106,7 +106,6 @@ namespace osu.Game.Tests.Online { var score = new SubmittableScore(new ScoreInfo { - Ruleset = new OsuRuleset().RulesetInfo, Mods = new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2 } } } }); diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 194341d1ab..33b1d9a67d 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -18,7 +18,6 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; @@ -28,7 +27,6 @@ using osu.Game.Screens.Play; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; -using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; using osuTK; using osuTK.Graphics; @@ -229,12 +227,7 @@ namespace osu.Game.Tests.Visual.Background FadeAccessibleResults results = null; - AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo - { - User = new APIUser { Username = "osu!" }, - BeatmapInfo = new TestBeatmap(Ruleset.Value).BeatmapInfo, - Ruleset = Ruleset.Value, - }))); + AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(TestResources.CreateTestScoreInfo()))); AddUntilStep("Wait for results is current", () => results.IsCurrentScreen()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index 745932315c..fa27e1abdd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -26,6 +26,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("total number of results == 1", () => { var score = new ScoreInfo(); + ((FailPlayer)Player).ScoreProcessor.PopulateScore(score); return score.Statistics.Values.Sum() == 1; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs index 744a2d187d..4674601f28 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs @@ -1,13 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using NUnit.Framework; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; -using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.Multiplayer { @@ -22,20 +20,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { var rulesetInfo = new OsuRuleset().RulesetInfo; var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo; - - var score = new ScoreInfo - { - Rank = ScoreRank.B, - TotalScore = 987654, - Accuracy = 0.8, - MaxCombo = 500, - Combo = 250, - BeatmapInfo = beatmapInfo, - User = new APIUser { Username = "Test user" }, - Date = DateTimeOffset.Now, - OnlineID = 12345, - Ruleset = rulesetInfo, - }; + var score = TestResources.CreateTestScoreInfo(beatmapInfo); PlaylistItem playlistItem = new PlaylistItem { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs index 99b6edc3b6..f5df8d7507 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs @@ -1,15 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Bindables; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; -using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.Multiplayer { @@ -26,20 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { var rulesetInfo = new OsuRuleset().RulesetInfo; var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo; - - var score = new ScoreInfo - { - Rank = ScoreRank.B, - TotalScore = 987654, - Accuracy = 0.8, - MaxCombo = 500, - Combo = 250, - BeatmapInfo = beatmapInfo, - User = new APIUser { Username = "Test user" }, - Date = DateTimeOffset.Now, - OnlineID = 12345, - Ruleset = rulesetInfo, - }; + var score = TestResources.CreateTestScoreInfo(beatmapInfo); PlaylistItem playlistItem = new PlaylistItem { diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 0494d1de3c..be390742ea 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -835,12 +835,7 @@ namespace osu.Game.Tests.Visual.SongSelect // this beatmap change should be overridden by the present. Beatmap.Value = manager.GetWorkingBeatmap(getSwitchBeatmap()); - songSelect.PresentScore(new ScoreInfo - { - User = new APIUser { Username = "woo" }, - BeatmapInfo = getPresentBeatmap(), - Ruleset = getPresentBeatmap().Ruleset - }); + songSelect.PresentScore(TestResources.CreateTestScoreInfo(getPresentBeatmap())); }); AddUntilStep("wait for results screen presented", () => !songSelect.IsCurrentScreen()); From 9e9341597d3ba0ecc05b36f88fbf8dcad59f1d1b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Dec 2021 17:59:04 +0900 Subject: [PATCH 325/419] Remove unused using statement --- osu.Game.Tests/Online/TestAPIModJsonSerialization.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs index 8b8954a1d0..4b160e1d67 100644 --- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs @@ -13,7 +13,6 @@ using osu.Game.Online.Solo; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; using osu.Game.Scoring; From c87ff82c1cde3af45c173fcb264de999340b743c Mon Sep 17 00:00:00 2001 From: rumoi Date: Tue, 14 Dec 2021 09:25:29 +1300 Subject: [PATCH 326/419] calculateRhythmBonus performance fix. --- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index d00d89a577..30d989cfba 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -55,7 +55,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills bool firstDeltaSwitch = false; - for (int i = Previous.Count - 2; i > 0; i--) + int rhythmStart = Math.Min(Previous.Count - 2, 0); + + while (rhythmStart < Previous.Count - 2 && current.StartTime - Previous[rhythmStart].StartTime < history_time_max) + rhythmStart++; + + for (int i = rhythmStart; i > 0; i--) { OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)Previous[i - 1]; OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)Previous[i]; From 6e3558b2226d834bd57146dec9bcd197ab441c30 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 Dec 2021 06:38:57 +0900 Subject: [PATCH 327/419] Remove weird test --- .../TestScenePlaylistsRoomCreation.cs | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index c5287d4257..450e821ba6 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -109,42 +109,6 @@ namespace osu.Game.Tests.Visual.Playlists AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom.Value.Playlist[0]); } - [Test] - public void TestBeatmapUpdatedOnReImport() - { - BeatmapSetInfo importedSet = null; - - AddStep("import altered beatmap", () => - { - IBeatmap beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); - - beatmap.BeatmapInfo.BaseDifficulty.CircleSize = 1; - - // intentionally increment online IDs to clash with import below. - beatmap.BeatmapInfo.OnlineID++; - beatmap.BeatmapInfo.BeatmapSet.OnlineID++; - - importedSet = manager.Import(beatmap.BeatmapInfo.BeatmapSet).Result.Value; - }); - - setupAndCreateRoom(room => - { - room.Name.Value = "my awesome room"; - room.Host.Value = API.LocalUser.Value; - room.Playlist.Add(new PlaylistItem - { - Beatmap = { Value = importedSet.Beatmaps[0] }, - Ruleset = { Value = new OsuRuleset().RulesetInfo } - }); - }); - - AddAssert("match has altered beatmap", () => match.Beatmap.Value.Beatmap.Difficulty.CircleSize == 1); - - importBeatmap(); - - AddAssert("match has original beatmap", () => match.Beatmap.Value.Beatmap.Difficulty.CircleSize != 1); - } - private void setupAndCreateRoom(Action room) { AddStep("setup room", () => room(SelectedRoom.Value)); From 7564658b5e01c780feadd62dccabbf1f8a051d31 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 Dec 2021 06:40:45 +0900 Subject: [PATCH 328/419] Reduce to 30s --- osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs | 2 +- osu.Game/Online/Solo/SubmitSoloScoreRequest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs index 4ee4be6164..e24d113822 100644 --- a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs @@ -31,7 +31,7 @@ namespace osu.Game.Online.Rooms req.ContentType = "application/json"; req.Method = HttpMethod.Put; - req.Timeout = 60000; + req.Timeout = 30000; req.AddRaw(JsonConvert.SerializeObject(score, new JsonSerializerSettings { diff --git a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs index 763fcf3f20..99cf5ceff5 100644 --- a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs +++ b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs @@ -31,7 +31,7 @@ namespace osu.Game.Online.Solo req.ContentType = "application/json"; req.Method = HttpMethod.Put; - req.Timeout = 60000; + req.Timeout = 30000; req.AddRaw(JsonConvert.SerializeObject(score, new JsonSerializerSettings { From 2f1dc912117f91443d912961e3f16df71ee11529 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 Dec 2021 11:30:42 +0900 Subject: [PATCH 329/419] Add AbortLoad() method to abort gameplay loads --- .../Multiplayer/IMultiplayerRoomServer.cs | 5 +++++ .../Online/Multiplayer/MultiplayerClient.cs | 2 ++ .../Multiplayer/OnlineMultiplayerClient.cs | 8 +++++++ .../OnlinePlay/Multiplayer/Multiplayer.cs | 21 +++++++++++++++++-- .../Multiplayer/TestMultiplayerClient.cs | 15 +++++++++++++ 5 files changed, 49 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 73fda78d00..200539def7 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -77,6 +77,11 @@ namespace osu.Game.Online.Multiplayer /// If an attempt to start the game occurs when the game's (or users') state disallows it. Task StartMatch(); + /// + /// Aborts an ongoing gameplay load. + /// + Task AbortLoad(); + /// /// Adds an item to the playlist. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 55b4def908..78d8362170 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -333,6 +333,8 @@ namespace osu.Game.Online.Multiplayer public abstract Task StartMatch(); + public abstract Task AbortLoad(); + public abstract Task AddPlaylistItem(MultiplayerPlaylistItem item); public abstract Task EditPlaylistItem(MultiplayerPlaylistItem item); diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index d268d2bf69..3062cf8b99 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -154,6 +154,14 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch)); } + public override Task AbortLoad() + { + if (!IsConnected.Value) + return Task.CompletedTask; + + return connection.InvokeAsync(nameof(IMultiplayerServer.AbortLoad)); + } + public override Task AddPlaylistItem(MultiplayerPlaylistItem item) { if (!IsConnected.Value) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 58b5b7bbeb..c299fd285a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.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.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Game.Online.Multiplayer; @@ -18,8 +19,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.OnResuming(last); - if (client.Room != null && client.LocalUser?.State != MultiplayerUserState.Spectating) - client.ChangeState(MultiplayerUserState.Idle); + if (client.Room == null) + return; + + Debug.Assert(client.LocalUser != null); + + switch (client.LocalUser.State) + { + case MultiplayerUserState.Spectating: + break; + + case MultiplayerUserState.WaitingForLoad: + client.AbortLoad(); + break; + + default: + client.ChangeState(MultiplayerUserState.Idle); + break; + } } protected override string ScreenTitle => "Multiplayer"; diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index d22f0415e6..c928cfd137 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -242,6 +242,11 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task ChangeState(MultiplayerUserState newState) { + Debug.Assert(Room != null); + + if (newState == MultiplayerUserState.Idle && LocalUser?.State == MultiplayerUserState.WaitingForLoad) + return Task.CompletedTask; + ChangeUserState(api.LocalUser.Value.Id, newState); return Task.CompletedTask; } @@ -303,6 +308,16 @@ namespace osu.Game.Tests.Visual.Multiplayer return ((IMultiplayerClient)this).LoadRequested(); } + public override Task AbortLoad() + { + Debug.Assert(Room != null); + Debug.Assert(LocalUser != null); + + ChangeUserState(LocalUser.UserID, MultiplayerUserState.Idle); + + return Task.CompletedTask; + } + public async Task AddUserPlaylistItem(int userId, MultiplayerPlaylistItem item) { Debug.Assert(Room != null); From 750bfae9092095881432f0ab35eee1bbcbf068a5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 Dec 2021 11:35:56 +0900 Subject: [PATCH 330/419] Fix TestMultiplayerClient not handling all users bailing from gameplay --- .../Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index c928cfd137..4c69f8f9d2 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -128,6 +128,15 @@ namespace osu.Game.Tests.Visual.Multiplayer case MultiplayerRoomState.WaitingForLoad: if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad)) { + var loadedUsers = Room.Users.Where(u => u.State == MultiplayerUserState.Loaded).ToArray(); + + if (loadedUsers.Length == 0) + { + // all users have bailed from the load sequence. cancel the game start. + ChangeRoomState(MultiplayerRoomState.Open); + return; + } + foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded)) ChangeUserState(u.UserID, MultiplayerUserState.Playing); @@ -143,8 +152,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.FinishedPlay)) ChangeUserState(u.UserID, MultiplayerUserState.Results); - ChangeRoomState(MultiplayerRoomState.Open); + ChangeRoomState(MultiplayerRoomState.Open); ((IMultiplayerClient)this).ResultsReady(); FinishCurrentItem().Wait(); From 357a6613790bf82f6bb1fccd0742317bcbd96e70 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 Dec 2021 13:13:12 +0900 Subject: [PATCH 331/419] Fix storyboard sprites sometimes starting too early --- osu.Game/Storyboards/StoryboardSprite.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 6fb2f5994b..f941cec20c 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -33,10 +33,8 @@ namespace osu.Game.Storyboards foreach (var l in loops) { - if (!(l.EarliestDisplayedTime is double lEarliest)) - continue; - - earliestStartTime = Math.Min(earliestStartTime, lEarliest); + if (l.EarliestDisplayedTime != null) + earliestStartTime = Math.Min(earliestStartTime, l.StartTime); } if (earliestStartTime < double.MaxValue) From 7e576ae9d3adc3133c03437e705a9a8879997205 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 13:25:19 +0900 Subject: [PATCH 332/419] Add note about how the background colour is restored --- osu.Game/Screens/Play/FailAnimation.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index aa777f18a8..cfbfdc9966 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -145,6 +145,7 @@ namespace osu.Game.Screens.Play Content.RotateTo(1, duration, Easing.OutQuart); Content.FadeColour(Color4.Gray, duration); + // Will be restored by `ApplyToBackground` logic in `SongSelect`. Background?.FadeColour(OsuColour.Gray(0.3f), 60); } From 8e6c7eb030fa523a6fe64273e4480b24fa6e49bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 13:52:27 +0900 Subject: [PATCH 333/419] Use `OsuStorage` in realm tests to allow for migration Also changes the realm filename to use `client` to match the ignore rules in `OsuStorage`. Without doing this, migration will fail in an indefinite mutex wait when attempting to delete the realm `.note` file. --- osu.Game.Tests/Database/RealmTest.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs index 04c9f2577a..6904464485 100644 --- a/osu.Game.Tests/Database/RealmTest.cs +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -10,6 +10,7 @@ using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Database; +using osu.Game.IO; using osu.Game.Models; #nullable enable @@ -27,15 +28,16 @@ namespace osu.Game.Tests.Database storage.DeleteDirectory(string.Empty); } - protected void RunTestWithRealm(Action testAction, [CallerMemberName] string caller = "") + protected void RunTestWithRealm(Action testAction, [CallerMemberName] string caller = "") { using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller)) { host.Run(new RealmTestGame(() => { - var testStorage = storage.GetStorageForDirectory(caller); + // ReSharper disable once AccessToDisposedClosure + var testStorage = new OsuStorage(host, storage.GetStorageForDirectory(caller)); - using (var realmFactory = new RealmContextFactory(testStorage, caller)) + using (var realmFactory = new RealmContextFactory(testStorage, "client")) { Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); testAction(realmFactory, testStorage); @@ -58,7 +60,7 @@ namespace osu.Game.Tests.Database { var testStorage = storage.GetStorageForDirectory(caller); - using (var realmFactory = new RealmContextFactory(testStorage, caller)) + using (var realmFactory = new RealmContextFactory(testStorage, "client")) { Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); await testAction(realmFactory, testStorage); From be337b4acea61c1bfea6b05442ad5d0201d63d3d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 13:53:23 +0900 Subject: [PATCH 334/419] Add failing test coverage of `RealmLive` failing post storage migration --- osu.Game.Tests/Database/RealmLiveTests.cs | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 9b6769b788..1d197791a4 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using NUnit.Framework; +using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Models; using Realms; @@ -29,6 +30,33 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestAccessAfterStorageMigrate() + { + RunTestWithRealm((realmFactory, storage) => + { + var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); + + ILive liveBeatmap; + + using (var context = realmFactory.CreateContext()) + { + context.Write(r => r.Add(beatmap)); + + liveBeatmap = beatmap.ToLive(); + } + + using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target")) + { + migratedStorage.DeleteDirectory(string.Empty); + + storage.Migrate(migratedStorage); + + Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden)); + } + }); + } + [Test] public void TestAccessAfterAttach() { From f9a2db5ec6dbf7e87b0cf2c578d7f81be0aa0138 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 14:19:43 +0900 Subject: [PATCH 335/419] Add accessibility to realm factory via `IStorageResourceProvider` We might need to rename this class.. --- osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs | 2 ++ osu.Game/Beatmaps/WorkingBeatmapCache.cs | 2 ++ osu.Game/IO/IStorageResourceProvider.cs | 6 ++++++ osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs | 2 ++ osu.Game/Tests/Visual/SkinnableTestScene.cs | 2 ++ 5 files changed, 14 insertions(+) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 88f35976ad..3aab28886e 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -15,6 +15,7 @@ using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Audio; +using osu.Game.Database; using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -220,6 +221,7 @@ namespace osu.Game.Tests.Gameplay public AudioManager AudioManager => Audio; public IResourceStore Files => null; public new IResourceStore Resources => base.Resources; + public RealmContextFactory RealmContextFactory => null; public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null; #endregion diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 559685d3c4..449406eadf 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -15,6 +15,7 @@ using osu.Framework.Platform; using osu.Framework.Statistics; using osu.Framework.Testing; using osu.Game.Beatmaps.Formats; +using osu.Game.Database; using osu.Game.IO; using osu.Game.Skinning; using osu.Game.Storyboards; @@ -108,6 +109,7 @@ namespace osu.Game.Beatmaps TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; ITrackStore IBeatmapResourceProvider.Tracks => trackStore; AudioManager IStorageResourceProvider.AudioManager => audioManager; + RealmContextFactory IStorageResourceProvider.RealmContextFactory => null; IResourceStore IStorageResourceProvider.Files => files; IResourceStore IStorageResourceProvider.Resources => resources; IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore); diff --git a/osu.Game/IO/IStorageResourceProvider.cs b/osu.Game/IO/IStorageResourceProvider.cs index e4c97e18fa..950b5aae09 100644 --- a/osu.Game/IO/IStorageResourceProvider.cs +++ b/osu.Game/IO/IStorageResourceProvider.cs @@ -4,6 +4,7 @@ using osu.Framework.Audio; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Game.Database; namespace osu.Game.IO { @@ -24,6 +25,11 @@ namespace osu.Game.IO /// IResourceStore Resources { get; } + /// + /// Access realm. + /// + RealmContextFactory RealmContextFactory { get; } + /// /// Create a texture loader store based on an underlying data store. /// diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index 31a2071249..f919edecf7 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -14,6 +14,7 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; +using osu.Game.Database; using osu.Game.IO; using osu.Game.Models; using osu.Game.Rulesets; @@ -118,6 +119,7 @@ namespace osu.Game.Tests.Beatmaps public IResourceStore Files => userSkinResourceStore; public new IResourceStore Resources => base.Resources; public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null; + RealmContextFactory IStorageResourceProvider.RealmContextFactory => null; #endregion diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index cdd3e47930..22aac79056 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics.Sprites; using osu.Game.IO; using osu.Game.Rulesets; @@ -158,6 +159,7 @@ namespace osu.Game.Tests.Visual public IResourceStore Files => null; public new IResourceStore Resources => base.Resources; public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); + RealmContextFactory IStorageResourceProvider.RealmContextFactory => null; #endregion From 441b7baa93225658fd82ed78420fd597bcd8bb62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 14:21:23 +0900 Subject: [PATCH 336/419] Provide a realm factory to usages of `ToLive`/`RealmLive` --- .../Editor/TestSceneManiaComposeScreen.cs | 4 +-- osu.Game.Tests/Database/RealmLiveTests.cs | 22 +++++++------- .../TestSceneBackgroundScreenDefault.cs | 2 +- .../Gameplay/TestSceneBeatmapSkinFallbacks.cs | 2 +- osu.Game/Database/RealmLive.cs | 30 ++++++++++++------- osu.Game/Database/RealmObjectExtensions.cs | 20 ++++++++++--- osu.Game/OsuGame.cs | 4 +-- .../Overlays/Settings/Sections/SkinSection.cs | 6 ++-- osu.Game/Skinning/Skin.cs | 2 +- osu.Game/Skinning/SkinManager.cs | 13 ++++---- osu.Game/Stores/RealmArchiveModelImporter.cs | 6 ++-- 11 files changed, 67 insertions(+), 44 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs index 91f5f93905..a30e09cd29 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs @@ -56,13 +56,13 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor [Test] public void TestDefaultSkin() { - AddStep("set default skin", () => skins.CurrentSkinInfo.Value = DefaultSkin.CreateInfo().ToLive()); + AddStep("set default skin", () => skins.CurrentSkinInfo.Value = DefaultSkin.CreateInfo().ToLiveUnmanaged()); } [Test] public void TestLegacySkin() { - AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.CreateInfo().ToLive()); + AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.CreateInfo().ToLiveUnmanaged()); } } } diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 1d197791a4..06cb5a3607 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -22,9 +22,9 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmFactory, _) => { - ILive beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive(); + ILive beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive(realmFactory); - ILive beatmap2 = realmFactory.CreateContext().All().First().ToLive(); + ILive beatmap2 = realmFactory.CreateContext().All().First().ToLive(realmFactory); Assert.AreEqual(beatmap, beatmap2); }); @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Database { context.Write(r => r.Add(beatmap)); - liveBeatmap = beatmap.ToLive(); + liveBeatmap = beatmap.ToLive(realmFactory); } using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target")) @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Database { var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); - var liveBeatmap = beatmap.ToLive(); + var liveBeatmap = beatmap.ToLive(realmFactory); using (var context = realmFactory.CreateContext()) context.Write(r => r.Add(beatmap)); @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Database public void TestAccessNonManaged() { var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); - var liveBeatmap = beatmap.ToLive(); + var liveBeatmap = beatmap.ToLiveUnmanaged(); Assert.IsFalse(beatmap.Hidden); Assert.IsFalse(liveBeatmap.Value.Hidden); @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Database { var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(); + liveBeatmap = beatmap.ToLive(realmFactory); } }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); @@ -131,7 +131,7 @@ namespace osu.Game.Tests.Database { var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(); + liveBeatmap = beatmap.ToLive(realmFactory); } }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); @@ -151,7 +151,7 @@ namespace osu.Game.Tests.Database RunTestWithRealm((realmFactory, _) => { var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); - var liveBeatmap = beatmap.ToLive(); + var liveBeatmap = beatmap.ToLive(realmFactory); Assert.DoesNotThrow(() => { @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Database { var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(); + liveBeatmap = beatmap.ToLive(realmFactory); } }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); @@ -211,7 +211,7 @@ namespace osu.Game.Tests.Database { var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(); + liveBeatmap = beatmap.ToLive(realmFactory); } }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); @@ -250,7 +250,7 @@ namespace osu.Game.Tests.Database // not just a refresh from the resolved Live. threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(); + liveBeatmap = beatmap.ToLive(realmFactory); } }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs index bdd1b92c8d..4762d3cded 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs @@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual.Background private void setCustomSkin() { // feign a skin switch. this doesn't do anything except force CurrentSkin to become a LegacySkin. - AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo().ToLive()); + AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo().ToLiveUnmanaged()); } private void setDefaultSkin() => AddStep("set default skin", () => skins.CurrentSkinInfo.SetDefault()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index cccc962a3f..c5f56cae9e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("setup skins", () => { - skinManager.CurrentSkinInfo.Value = gameCurrentSkin.ToLive(); + skinManager.CurrentSkinInfo.Value = gameCurrentSkin.ToLiveUnmanaged(); currentBeatmapSkin = getBeatmapSkin(); }); }); diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index c376d5d503..4f7bdf93e4 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -24,14 +24,22 @@ namespace osu.Game.Database /// private readonly T data; + private readonly RealmContextFactory? realmFactory; + /// /// Construct a new instance of live realm data. /// /// The realm data. - public RealmLive(T data) + /// The realm factory the data was sourced from. May be null for an unmanaged object. + public RealmLive(T data, RealmContextFactory? realmFactory) { this.data = data; + if (IsManaged && realmFactory == null) + throw new ArgumentException(@"Realm factory must be provided for a managed instance", nameof(realmFactory)); + + this.realmFactory = realmFactory; + ID = data.ID; } @@ -47,7 +55,10 @@ namespace osu.Game.Database return; } - using (var realm = Realm.GetInstance(data.Realm.Config)) + if (realmFactory == null) + throw new ArgumentException(@"Realm factory must be provided for a managed instance", nameof(realmFactory)); + + using (var realm = realmFactory.CreateContext()) perform(realm.Find(ID)); } @@ -58,12 +69,15 @@ namespace osu.Game.Database public TReturn PerformRead(Func perform) { if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn))) - throw new InvalidOperationException($"Realm live objects should not exit the scope of {nameof(PerformRead)}."); + throw new InvalidOperationException(@$"Realm live objects should not exit the scope of {nameof(PerformRead)}."); if (!IsManaged) return perform(data); - using (var realm = Realm.GetInstance(data.Realm.Config)) + if (realmFactory == null) + throw new ArgumentException(@"Realm factory must be provided for a managed instance", nameof(realmFactory)); + + using (var realm = realmFactory.CreateContext()) return perform(realm.Find(ID)); } @@ -74,7 +88,7 @@ namespace osu.Game.Database public void PerformWrite(Action perform) { if (!IsManaged) - throw new InvalidOperationException("Can't perform writes on a non-managed underlying value"); + throw new InvalidOperationException(@"Can't perform writes on a non-managed underlying value"); PerformRead(t => { @@ -94,11 +108,7 @@ namespace osu.Game.Database if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException($"Can't use {nameof(Value)} on managed objects from non-update threads"); - // When using Value, we rely on garbage collection for the realm instance used to retrieve the instance. - // As we are sure that this is on the update thread, there should always be an open and constantly refreshing realm instance to ensure file size growth is a non-issue. - var realm = Realm.GetInstance(data.Realm.Config); - - return realm.Find(ID); + return realmFactory!.Context.Find(ID); } } diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index b38e21453c..c546a70fae 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -53,16 +53,28 @@ namespace osu.Game.Database return mapper.Map(item); } - public static List> ToLive(this IEnumerable realmList) + public static List> ToLiveUnmanaged(this IEnumerable realmList) where T : RealmObject, IHasGuidPrimaryKey { - return realmList.Select(l => new RealmLive(l)).Cast>().ToList(); + return realmList.Select(l => new RealmLive(l, null)).Cast>().ToList(); } - public static ILive ToLive(this T realmObject) + public static ILive ToLiveUnmanaged(this T realmObject) where T : RealmObject, IHasGuidPrimaryKey { - return new RealmLive(realmObject); + return new RealmLive(realmObject, null); + } + + public static List> ToLive(this IEnumerable realmList, RealmContextFactory realmContextFactory) + where T : RealmObject, IHasGuidPrimaryKey + { + return realmList.Select(l => new RealmLive(l, realmContextFactory)).Cast>().ToList(); + } + + public static ILive ToLive(this T realmObject, RealmContextFactory realmContextFactory) + where T : RealmObject, IHasGuidPrimaryKey + { + return new RealmLive(realmObject, realmContextFactory); } /// diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a35191613c..9c379de683 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -255,10 +255,10 @@ namespace osu.Game if (skinInfo == null) { if (guid == SkinInfo.CLASSIC_SKIN) - skinInfo = DefaultLegacySkin.CreateInfo().ToLive(); + skinInfo = DefaultLegacySkin.CreateInfo().ToLiveUnmanaged(); } - SkinManager.CurrentSkinInfo.Value = skinInfo ?? DefaultSkin.CreateInfo().ToLive(); + SkinManager.CurrentSkinInfo.Value = skinInfo ?? DefaultSkin.CreateInfo().ToLiveUnmanaged(); }; configSkin.TriggerChange(); diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index b1582d7bee..0fa6d78d4b 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -32,14 +32,14 @@ namespace osu.Game.Overlays.Settings.Sections Icon = FontAwesome.Solid.PaintBrush }; - private readonly Bindable> dropdownBindable = new Bindable> { Default = DefaultSkin.CreateInfo().ToLive() }; + private readonly Bindable> dropdownBindable = new Bindable> { Default = DefaultSkin.CreateInfo().ToLiveUnmanaged() }; private readonly Bindable configBindable = new Bindable(); private static readonly ILive random_skin_info = new SkinInfo { ID = SkinInfo.RANDOM_SKIN, Name = "", - }.ToLive(); + }.ToLiveUnmanaged(); private List> skinItems; @@ -133,7 +133,7 @@ namespace osu.Game.Overlays.Settings.Sections { int protectedCount = realmSkins.Count(s => s.Protected); - skinItems = realmSkins.ToLive(); + skinItems = realmSkins.ToLive(realmFactory); skinItems.Insert(protectedCount, random_skin_info); diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 54fc2340f1..ee92b6b40a 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -43,7 +43,7 @@ namespace osu.Game.Skinning protected Skin(SkinInfo skin, IStorageResourceProvider resources, [CanBeNull] Stream configurationStream = null) { - SkinInfo = skin.ToLive(); + SkinInfo = skin.ToLive(resources.RealmContextFactory); this.resources = resources; configurationStream ??= getConfigurationStream(); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 5134632fb1..bb2f0a37b4 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -47,9 +47,9 @@ namespace osu.Game.Skinning public readonly Bindable CurrentSkin = new Bindable(); - public readonly Bindable> CurrentSkinInfo = new Bindable>(Skinning.DefaultSkin.CreateInfo().ToLive()) + public readonly Bindable> CurrentSkinInfo = new Bindable>(Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged()) { - Default = Skinning.DefaultSkin.CreateInfo().ToLive() + Default = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged() }; private readonly SkinModelManager skinModelManager; @@ -119,13 +119,13 @@ namespace osu.Game.Skinning if (randomChoices.Length == 0) { - CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLive(); + CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged(); return; } var chosen = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length)); - CurrentSkinInfo.Value = chosen.ToLive(); + CurrentSkinInfo.Value = chosen.ToLive(contextFactory); } } @@ -182,7 +182,7 @@ namespace osu.Game.Skinning public ILive Query(Expression> query) { using (var context = contextFactory.CreateContext()) - return context.All().FirstOrDefault(query)?.ToLive(); + return context.All().FirstOrDefault(query)?.ToLive(contextFactory); } public event Action SourceChanged; @@ -237,6 +237,7 @@ namespace osu.Game.Skinning AudioManager IStorageResourceProvider.AudioManager => audio; IResourceStore IStorageResourceProvider.Resources => resources; IResourceStore IStorageResourceProvider.Files => userFiles; + RealmContextFactory IStorageResourceProvider.RealmContextFactory => contextFactory; IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); #endregion @@ -302,7 +303,7 @@ namespace osu.Game.Skinning Guid currentUserSkin = CurrentSkinInfo.Value.ID; if (items.Any(s => s.ID == currentUserSkin)) - scheduler.Add(() => CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLive()); + scheduler.Add(() => CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged()); skinModelManager.Delete(items.ToList(), silent); } diff --git a/osu.Game/Stores/RealmArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs index 1681dad750..4aca079e2e 100644 --- a/osu.Game/Stores/RealmArchiveModelImporter.cs +++ b/osu.Game/Stores/RealmArchiveModelImporter.cs @@ -352,7 +352,7 @@ namespace osu.Game.Stores transaction.Commit(); } - return existing.ToLive(); + return existing.ToLive(ContextFactory); } LogForModel(item, @"Found existing (optimised) but failed pre-check."); @@ -387,7 +387,7 @@ namespace osu.Game.Stores existing.DeletePending = false; transaction.Commit(); - return existing.ToLive(); + return existing.ToLive(ContextFactory); } LogForModel(item, @"Found existing but failed re-use check."); @@ -416,7 +416,7 @@ namespace osu.Game.Stores throw; } - return item.ToLive(); + return item.ToLive(ContextFactory); } } From eb3050b2ac9fd4245d9006eb300e844ed90c3178 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 Dec 2021 15:08:00 +0900 Subject: [PATCH 337/419] Fix incorrect test --- osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index f5f17a0bc1..e03c8d7561 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -85,11 +85,12 @@ namespace osu.Game.Tests.Visual.Gameplay loopGroup.Scale.Add(Easing.None, -20000, -18000, 0, 1); var target = addEventToLoop ? loopGroup : sprite.TimelineGroup; - target.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1); + double targetTime = addEventToLoop ? 20000 : 0; + target.Alpha.Add(Easing.None, targetTime + firstStoryboardEvent, targetTime + firstStoryboardEvent + 500, 0, 1); // these should be ignored due to being in the future. sprite.TimelineGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1); - loopGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1); + loopGroup.Alpha.Add(Easing.None, 38000, 40000, 0, 1); storyboard.GetLayer("Background").Add(sprite); From b6a272e31a0fe8300761532c5688b3b0320d6e4a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 16:10:53 +0900 Subject: [PATCH 338/419] Add failing test coverage of `BackgroundScreeNDefault`'s beatmap background tracking when active/non-active --- .../TestSceneBackgroundScreenDefault.cs | 134 +++++++++++++++++- .../Backgrounds/BackgroundScreenDefault.cs | 2 +- 2 files changed, 129 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs index bdd1b92c8d..476eadf9bb 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs @@ -5,8 +5,11 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Graphics.Textures; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics.Backgrounds; @@ -15,6 +18,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens; using osu.Game.Screens.Backgrounds; using osu.Game.Skinning; +using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual.Background { @@ -22,8 +26,7 @@ namespace osu.Game.Tests.Visual.Background public class TestSceneBackgroundScreenDefault : OsuTestScene { private BackgroundScreenStack stack; - private BackgroundScreenDefault screen; - + private TestBackgroundScreenDefault screen; private Graphics.Backgrounds.Background getCurrentBackground() => screen.ChildrenOfType().FirstOrDefault(); [Resolved] @@ -36,10 +39,95 @@ namespace osu.Game.Tests.Visual.Background public void SetUpSteps() { AddStep("create background stack", () => Child = stack = new BackgroundScreenStack()); - AddStep("push default screen", () => stack.Push(screen = new BackgroundScreenDefault(false))); + AddStep("push default screen", () => stack.Push(screen = new TestBackgroundScreenDefault())); AddUntilStep("wait for screen to load", () => screen.IsCurrentScreen()); } + [Test] + public void TestBeatmapBackgroundTracksBeatmap() + { + setSupporter(true); + setSourceMode(BackgroundSource.Beatmap); + + AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground()); + AddAssert("background changed", () => screen.CheckLastLoadChange() == true); + + Graphics.Backgrounds.Background last = null; + + AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackground)); + AddStep("store background", () => last = getCurrentBackground()); + + AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground()); + + AddUntilStep("wait for beatmap background to change", () => screen.CheckLastLoadChange() == true); + + AddUntilStep("background is new beatmap background", () => last != getCurrentBackground()); + AddStep("store background", () => last = getCurrentBackground()); + + AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground()); + + AddUntilStep("wait for beatmap background to change", () => screen.CheckLastLoadChange() == true); + AddUntilStep("background is new beatmap background", () => last != getCurrentBackground()); + } + + [Test] + public void TestBeatmapBackgroundTracksBeatmapWhenSuspended() + { + setSupporter(true); + setSourceMode(BackgroundSource.Beatmap); + + AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground()); + AddAssert("background changed", () => screen.CheckLastLoadChange() == true); + AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackground)); + + BackgroundScreenBeatmap nestedScreen = null; + + // of note, this needs to be a type that doesn't match BackgroundScreenDefault else it is silently not pushed by the background stack. + AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value))); + AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen()); + + AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null); + + AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground()); + + AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null); + + AddStep("pop screen back to top level", () => screen.MakeCurrent()); + + AddAssert("top level background changed", () => screen.CheckLastLoadChange() == true); + } + + [Test] + public void TestBeatmapBackgroundIgnoresNoChangeWhenSuspended() + { + BackgroundScreenBeatmap nestedScreen = null; + WorkingBeatmap originalWorking = null; + + setSupporter(true); + setSourceMode(BackgroundSource.Beatmap); + + AddStep("change beatmap", () => originalWorking = Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground()); + AddAssert("background changed", () => screen.CheckLastLoadChange() == true); + AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackground)); + + // of note, this needs to be a type that doesn't match BackgroundScreenDefault else it is silently not pushed by the background stack. + AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value))); + AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen()); + + // we're testing a case where scheduling may be used to avoid issues, so ensure the scheduler is no longer running. + AddUntilStep("wait for top level not alive", () => !screen.IsAlive); + + AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground()); + AddStep("change beatmap back", () => Beatmap.Value = originalWorking); + + AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null); + + AddStep("pop screen back to top level", () => screen.MakeCurrent()); + + AddStep("top level screen is current", () => screen.IsCurrentScreen()); + AddAssert("top level background reused existing", () => screen.CheckLastLoadChange() == false); + } + [Test] public void TestBackgroundTypeSwitch() { @@ -96,13 +184,11 @@ namespace osu.Game.Tests.Visual.Background [Test] public void TestBackgroundCyclingOnDefaultSkin([Values] bool supporter) { - Graphics.Backgrounds.Background last = null; - setSourceMode(BackgroundSource.Skin); setSupporter(supporter); setDefaultSkin(); - AddUntilStep("wait for beatmap background to be loaded", () => (last = getCurrentBackground())?.GetType() == typeof(Graphics.Backgrounds.Background)); + AddUntilStep("wait for beatmap background to be loaded", () => (getCurrentBackground())?.GetType() == typeof(Graphics.Backgrounds.Background)); AddAssert("next cycles background", () => screen.Next()); // doesn't really need to be checked but might as well. @@ -120,6 +206,42 @@ namespace osu.Game.Tests.Visual.Background Id = API.LocalUser.Value.Id + 1, }); + private WorkingBeatmap createTestWorkingBeatmapWithUniqueBackground() => new UniqueBackgroundTestWorkingBeatmap(Audio); + + private class TestBackgroundScreenDefault : BackgroundScreenDefault + { + private bool? lastLoadTriggerCausedChange; + + public TestBackgroundScreenDefault() + : base(false) + { + } + + public override bool Next() + { + bool didChange = base.Next(); + lastLoadTriggerCausedChange = didChange; + return didChange; + } + + public bool? CheckLastLoadChange() + { + bool? lastChange = lastLoadTriggerCausedChange; + lastLoadTriggerCausedChange = null; + return lastChange; + } + } + + private class UniqueBackgroundTestWorkingBeatmap : TestWorkingBeatmap + { + public UniqueBackgroundTestWorkingBeatmap(AudioManager audioManager) + : base(new Beatmap(), null, audioManager) + { + } + + protected override Texture GetBackground() => new Texture(1, 1); + } + private void setCustomSkin() { // feign a skin switch. this doesn't do anything except force CurrentSkin to become a LegacySkin. diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 4a922c45b9..a1b1b52c14 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Backgrounds /// Request loading the next background. /// /// Whether a new background was queued for load. May return false if the current background is still valid. - public bool Next() + public virtual bool Next() { var nextBackground = createBackground(); From 25a056dfad45269be89e509a6554ad3c61ca02f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 16:35:18 +0900 Subject: [PATCH 339/419] Remove pointless/broken test steps These aren't accurate and are tested via a more accurate means directly above. --- .../Background/TestSceneBackgroundScreenDefault.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs index 476eadf9bb..1d3dd35e1d 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs @@ -166,19 +166,13 @@ namespace osu.Game.Tests.Visual.Background [TestCase(BackgroundSource.Skin, typeof(SkinBackground))] public void TestBackgroundDoesntReloadOnNoChange(BackgroundSource source, Type backgroundType) { - Graphics.Backgrounds.Background last = null; - setSourceMode(source); setSupporter(true); if (source == BackgroundSource.Skin) setCustomSkin(); - AddUntilStep("wait for beatmap background to be loaded", () => (last = getCurrentBackground())?.GetType() == backgroundType); + AddUntilStep("wait for beatmap background to be loaded", () => (getCurrentBackground())?.GetType() == backgroundType); AddAssert("next doesn't load new background", () => screen.Next() == false); - - // doesn't really need to be checked but might as well. - AddWaitStep("wait a bit", 5); - AddUntilStep("ensure same background instance", () => last == getCurrentBackground()); } [Test] @@ -190,10 +184,6 @@ namespace osu.Game.Tests.Visual.Background AddUntilStep("wait for beatmap background to be loaded", () => (getCurrentBackground())?.GetType() == typeof(Graphics.Backgrounds.Background)); AddAssert("next cycles background", () => screen.Next()); - - // doesn't really need to be checked but might as well. - AddWaitStep("wait a bit", 5); - AddUntilStep("ensure different background instance", () => last != getCurrentBackground()); } private void setSourceMode(BackgroundSource source) => From 8c6f50ddb1b3f7993485a06129803e442b6061fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 15:17:01 +0900 Subject: [PATCH 340/419] Fix `BackgroundScreenDefault` incorrectly updating current background after being inactive If the beatmap was changed but then reverted to the previously displayed map, the background may have displayed incorrectly on resuming. Closes #15804. --- .../Backgrounds/BackgroundScreenDefault.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index a1b1b52c14..452f033dcc 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -48,16 +48,19 @@ namespace osu.Game.Screens.Backgrounds AddInternal(seasonalBackgroundLoader); - user.ValueChanged += _ => Next(); - skin.ValueChanged += _ => Next(); - mode.ValueChanged += _ => Next(); - beatmap.ValueChanged += _ => Next(); - introSequence.ValueChanged += _ => Next(); - seasonalBackgroundLoader.SeasonalBackgroundChanged += () => Next(); + user.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired); + skin.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired); + mode.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired); + beatmap.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired); + introSequence.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired); + seasonalBackgroundLoader.SeasonalBackgroundChanged += () => Scheduler.AddOnce(loadNextIfRequired); currentDisplay = RNG.Next(0, background_count); Next(); + + // helper function required for AddOnce usage. + void loadNextIfRequired() => Next(); } private ScheduledDelegate nextTask; From 79dd9674fc5a580a10d3005d272a1b94b4aaff4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 17:41:02 +0900 Subject: [PATCH 341/419] Use longer form to read better Using `l.StartTime` reads like a coding issue, even though if you go down the call chain looks to be correct. --- osu.Game/Storyboards/StoryboardSprite.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index f941cec20c..ebd1a941a8 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -33,8 +33,8 @@ namespace osu.Game.Storyboards foreach (var l in loops) { - if (l.EarliestDisplayedTime != null) - earliestStartTime = Math.Min(earliestStartTime, l.StartTime); + if (l.EarliestDisplayedTime is double loopEarliestDisplayTime) + earliestStartTime = Math.Min(earliestStartTime, l.LoopStartTime + loopEarliestDisplayTime); } if (earliestStartTime < double.MaxValue) From 3bc2de4889cb0874c9a3d4adb0392ab06523f10a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 19:11:23 +0900 Subject: [PATCH 342/419] Add failing test coverage of modified beatmap import breaking online availability state --- .../TestSceneOnlinePlayBeatmapAvailabilityTracker.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 24824b1e23..239c787349 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -114,18 +114,23 @@ namespace osu.Game.Tests.Online public void TestTrackerRespectsChecksum() { AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); + AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).Wait()); + addAvailabilityCheckStep("initially locally available", BeatmapAvailability.LocallyAvailable); AddStep("import altered beatmap", () => { beatmaps.Import(TestResources.GetTestBeatmapForImport(true)).Wait(); }); - addAvailabilityCheckStep("state still not downloaded", BeatmapAvailability.NotDownloaded); + addAvailabilityCheckStep("state not downloaded", BeatmapAvailability.NotDownloaded); AddStep("recreate tracker", () => Child = availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker { SelectedItem = { BindTarget = selectedItem } }); addAvailabilityCheckStep("state not downloaded as well", BeatmapAvailability.NotDownloaded); + + AddStep("reimport original beatmap", () => beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait()); + addAvailabilityCheckStep("locally available after re-import", BeatmapAvailability.LocallyAvailable); } private void addAvailabilityCheckStep(string description, Func expected) From 453ecd21b32b552947ee1141f793e26d88774113 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 19:11:56 +0900 Subject: [PATCH 343/419] Fix `OnlinePlayBeatmapAvailabilityTracker` potentially in incorrect state Adter an import of a modified version of a beatmap (that was already present in the local database), it's feasible that one of these trackers would not see the state change due to the nuances of the import process. --- .../OnlinePlayBeatmapAvailabilityTracker.cs | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index aa0e37363b..a32f069470 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -40,6 +40,11 @@ namespace osu.Game.Online.Rooms private BeatmapDownloadTracker downloadTracker; + /// + /// The beatmap matching the required hash (and providing a final state). + /// + private BeatmapInfo matchingHash; + protected override void LoadComplete() { base.LoadComplete(); @@ -71,13 +76,34 @@ namespace osu.Game.Online.Rooms progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500); }, true); }, true); + + // These events are needed for a fringe case where a modified/altered beatmap is imported with matching OnlineIDs. + // During the import process this will cause the existing beatmap set to be silently deleted and replaced with the new one. + // This is not exposed to us via `BeatmapDownloadTracker` so we have to take it into our own hands (as we care about the hash matching). + beatmapManager.ItemUpdated += itemUpdated; + beatmapManager.ItemRemoved += itemRemoved; } + private void itemUpdated(BeatmapSetInfo item) => Schedule(() => + { + if (matchingHash?.BeatmapSet.ID == item.ID || SelectedItem.Value?.Beatmap.Value.BeatmapSet?.OnlineID == item.OnlineID) + updateAvailability(); + }); + + private void itemRemoved(BeatmapSetInfo item) => Schedule(() => + { + if (matchingHash?.BeatmapSet.ID == item.ID) + updateAvailability(); + }); + private void updateAvailability() { if (downloadTracker == null) return; + // will be repopulated below if still valid. + matchingHash = null; + switch (downloadTracker.State.Value) { case DownloadState.NotDownloaded: @@ -93,7 +119,9 @@ namespace osu.Game.Online.Rooms break; case DownloadState.LocallyAvailable: - bool hashMatches = checkHashValidity(); + matchingHash = findMatchingHash(); + + bool hashMatches = matchingHash != null; availability.Value = hashMatches ? BeatmapAvailability.LocallyAvailable() : BeatmapAvailability.NotDownloaded(); @@ -108,12 +136,23 @@ namespace osu.Game.Online.Rooms } } - private bool checkHashValidity() + private BeatmapInfo findMatchingHash() { int onlineId = SelectedItem.Value.Beatmap.Value.OnlineID; string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash; - return beatmapManager.QueryBeatmap(b => b.OnlineID == onlineId && b.MD5Hash == checksum && !b.BeatmapSet.DeletePending) != null; + return beatmapManager.QueryBeatmap(b => b.OnlineID == onlineId && b.MD5Hash == checksum && !b.BeatmapSet.DeletePending); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (beatmapManager != null) + { + beatmapManager.ItemUpdated -= itemUpdated; + beatmapManager.ItemRemoved -= itemRemoved; + } } } } From 0950d8d327eb9d8a85564c917826461dd83b4c6f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 19:16:51 +0900 Subject: [PATCH 344/419] Add back `PlaylistRoomCreation` test Was spiritually removed in https://github.com/ppy/osu/pull/16045. This implementation is mostly taken from that PR's comment thread verbatim, and now works due to the associated changes to `OnlinePlayBeatmapAvailabilityTracker`. --- .../TestScenePlaylistsRoomCreation.cs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index 450e821ba6..9dd8b41dcd 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -15,9 +15,11 @@ using osu.Game.Database; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; +using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Visual.OnlinePlay; using osuTK.Input; @@ -109,7 +111,87 @@ namespace osu.Game.Tests.Visual.Playlists AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom.Value.Playlist[0]); } + [Test] + public void TestBeatmapUpdatedOnReImport() + { + string realHash = null; + int realOnlineId = 0; + int realOnlineSetId = 0; + + AddStep("store real beatmap values", () => + { + realHash = importedBeatmap.Value.Beatmaps[0].MD5Hash; + realOnlineId = importedBeatmap.Value.Beatmaps[0].OnlineID ?? -1; + realOnlineSetId = importedBeatmap.Value.OnlineID ?? -1; + }); + + AddStep("import modified beatmap", () => + { + var modifiedBeatmap = new TestBeatmap(new OsuRuleset().RulesetInfo) + { + BeatmapInfo = + { + OnlineID = realOnlineId, + BeatmapSet = + { + OnlineID = realOnlineSetId + } + }, + }; + + modifiedBeatmap.HitObjects.Clear(); + modifiedBeatmap.HitObjects.Add(new HitCircle { StartTime = 5000 }); + + manager.Import(modifiedBeatmap.BeatmapInfo.BeatmapSet).Wait(); + }); + + // Create the room using the real beatmap values. + setupAndCreateRoom(room => + { + room.Name.Value = "my awesome room"; + room.Host.Value = API.LocalUser.Value; + room.Playlist.Add(new PlaylistItem + { + Beatmap = + { + Value = new BeatmapInfo + { + MD5Hash = realHash, + OnlineID = realOnlineId, + BeatmapSet = new BeatmapSetInfo + { + OnlineID = realOnlineSetId, + } + } + }, + Ruleset = { Value = new OsuRuleset().RulesetInfo } + }); + }); + + AddAssert("match has default beatmap", () => match.Beatmap.IsDefault); + + AddStep("reimport original beatmap", () => + { + var originalBeatmap = new TestBeatmap(new OsuRuleset().RulesetInfo) + { + BeatmapInfo = + { + OnlineID = realOnlineId, + BeatmapSet = + { + OnlineID = realOnlineSetId + } + }, + }; + + manager.Import(originalBeatmap.BeatmapInfo.BeatmapSet).Wait(); + }); + + AddUntilStep("match has correct beatmap", () => realHash == match.Beatmap.Value.BeatmapInfo.MD5Hash); + } + private void setupAndCreateRoom(Action room) + { AddStep("setup room", () => room(SelectedRoom.Value)); From 8e79fac389d5f5ad5b90a15c2da7d19dc7219f70 Mon Sep 17 00:00:00 2001 From: tbrose Date: Tue, 14 Dec 2021 16:23:51 +0100 Subject: [PATCH 345/419] Fixes code quality check failed --- .../Online/Chat/MessageNotifierTest.cs | 26 +++++++++---------- osu.Game/Online/Chat/MessageNotifier.cs | 5 ++-- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs b/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs index b885299d1f..2ec5b778d1 100644 --- a/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs +++ b/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs @@ -12,79 +12,79 @@ namespace osu.Game.Tests.Online.Chat [Test] public void TestContainsUsernameMidlinePositive() { - Assert.IsTrue(MessageNotifier.checkContainsUsername("This is a test message", "Test")); + Assert.IsTrue(MessageNotifier.CheckContainsUsername("This is a test message", "Test")); } [Test] public void TestContainsUsernameStartOfLinePositive() { - Assert.IsTrue(MessageNotifier.checkContainsUsername("Test message", "Test")); + Assert.IsTrue(MessageNotifier.CheckContainsUsername("Test message", "Test")); } [Test] public void TestContainsUsernameEndOfLinePositive() { - Assert.IsTrue(MessageNotifier.checkContainsUsername("This is a test", "Test")); + Assert.IsTrue(MessageNotifier.CheckContainsUsername("This is a test", "Test")); } [Test] public void TestContainsUsernameMidlineNegative() { - Assert.IsFalse(MessageNotifier.checkContainsUsername("This is a testmessage for notifications", "Test")); + Assert.IsFalse(MessageNotifier.CheckContainsUsername("This is a testmessage for notifications", "Test")); } [Test] public void TestContainsUsernameStartOfLineNegative() { - Assert.IsFalse(MessageNotifier.checkContainsUsername("Testmessage", "Test")); + Assert.IsFalse(MessageNotifier.CheckContainsUsername("Testmessage", "Test")); } [Test] public void TestContainsUsernameEndOfLineNegative() { - Assert.IsFalse(MessageNotifier.checkContainsUsername("This is a notificationtest", "Test")); + Assert.IsFalse(MessageNotifier.CheckContainsUsername("This is a notificationtest", "Test")); } [Test] public void TestContainsUsernameBetweenInterpunction() { - Assert.IsTrue(MessageNotifier.checkContainsUsername("Hello 'test'-message", "Test")); + Assert.IsTrue(MessageNotifier.CheckContainsUsername("Hello 'test'-message", "Test")); } [Test] public void TestContainsUsernameUnicode() { - Assert.IsTrue(MessageNotifier.checkContainsUsername("Test \u0460\u0460 message", "\u0460\u0460")); + Assert.IsTrue(MessageNotifier.CheckContainsUsername("Test \u0460\u0460 message", "\u0460\u0460")); } [Test] public void TestContainsUsernameUnicodeNegative() { - Assert.IsFalse(MessageNotifier.checkContainsUsername("Test ha\u0460\u0460o message", "\u0460\u0460")); + Assert.IsFalse(MessageNotifier.CheckContainsUsername("Test ha\u0460\u0460o message", "\u0460\u0460")); } [Test] public void TestContainsUsernameSpecialCharactersPositive() { - Assert.IsTrue(MessageNotifier.checkContainsUsername("Test [#^-^#] message", "[#^-^#]")); + Assert.IsTrue(MessageNotifier.CheckContainsUsername("Test [#^-^#] message", "[#^-^#]")); } [Test] public void TestContainsUsernameSpecialCharactersNegative() { - Assert.IsFalse(MessageNotifier.checkContainsUsername("Test pad[#^-^#]oru message", "[#^-^#]")); + Assert.IsFalse(MessageNotifier.CheckContainsUsername("Test pad[#^-^#]oru message", "[#^-^#]")); } [Test] public void TestContainsUsernameAtSign() { - Assert.IsTrue(MessageNotifier.checkContainsUsername("@username hi", "username")); + Assert.IsTrue(MessageNotifier.CheckContainsUsername("@username hi", "username")); } [Test] public void TestContainsUsernameColon() { - Assert.IsTrue(MessageNotifier.checkContainsUsername("username: hi", "username")); + Assert.IsTrue(MessageNotifier.CheckContainsUsername("username: hi", "username")); } } } diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index db7c5e47f5..a11af7b305 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; @@ -121,7 +120,7 @@ namespace osu.Game.Online.Chat private void checkForMentions(Channel channel, Message message) { - if (!notifyOnUsername.Value || !checkContainsUsername(message.Content, localUser.Value.Username)) return; + if (!notifyOnUsername.Value || !CheckContainsUsername(message.Content, localUser.Value.Username)) return; notifications.Post(new MentionNotification(message.Sender.Username, channel)); } @@ -130,7 +129,7 @@ namespace osu.Game.Online.Chat /// Checks if mentions . /// This will match against the case where underscores are used instead of spaces (which is how osu-stable handles usernames with spaces). /// - public static bool checkContainsUsername(string message, string username) + public static bool CheckContainsUsername(string message, string username) { string fullName = Regex.Escape(username); string underscoreName = Regex.Escape(username.Replace(' ', '_')); From 4664bb1d29ad5d8d442f97f94e12d5bdc7e57bc0 Mon Sep 17 00:00:00 2001 From: rumoi Date: Wed, 15 Dec 2021 05:16:10 +1300 Subject: [PATCH 346/419] Remove uneeded complexity --- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 30d989cfba..d26808bf04 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills bool firstDeltaSwitch = false; - int rhythmStart = Math.Min(Previous.Count - 2, 0); + int rhythmStart = 0; while (rhythmStart < Previous.Count - 2 && current.StartTime - Previous[rhythmStart].StartTime < history_time_max) rhythmStart++; From 9ade8069a1b7ded46328c023eeb119e26900d1ce Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 Dec 2021 16:52:57 +0900 Subject: [PATCH 347/419] Rename to AbortGameplay() and handle additional states --- osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs | 2 +- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 2 +- osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs | 4 ++-- osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs | 4 +++- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 200539def7..073d512f90 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -80,7 +80,7 @@ namespace osu.Game.Online.Multiplayer /// /// Aborts an ongoing gameplay load. /// - Task AbortLoad(); + Task AbortGameplay(); /// /// Adds an item to the playlist. diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 78d8362170..ffb9e193d4 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -333,7 +333,7 @@ namespace osu.Game.Online.Multiplayer public abstract Task StartMatch(); - public abstract Task AbortLoad(); + public abstract Task AbortGameplay(); public abstract Task AddPlaylistItem(MultiplayerPlaylistItem item); diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 3062cf8b99..73b87190b1 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -154,12 +154,12 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch)); } - public override Task AbortLoad() + public override Task AbortGameplay() { if (!IsConnected.Value) return Task.CompletedTask; - return connection.InvokeAsync(nameof(IMultiplayerServer.AbortLoad)); + return connection.InvokeAsync(nameof(IMultiplayerServer.AbortGameplay)); } public override Task AddPlaylistItem(MultiplayerPlaylistItem item) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index c299fd285a..e136627d43 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -30,7 +30,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer break; case MultiplayerUserState.WaitingForLoad: - client.AbortLoad(); + case MultiplayerUserState.Loaded: + case MultiplayerUserState.Playing: + client.AbortGameplay(); break; default: diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 4c69f8f9d2..767751a808 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -317,7 +317,7 @@ namespace osu.Game.Tests.Visual.Multiplayer return ((IMultiplayerClient)this).LoadRequested(); } - public override Task AbortLoad() + public override Task AbortGameplay() { Debug.Assert(Room != null); Debug.Assert(LocalUser != null); From da00c020be8cd32aa1ee870d242337fb3cc60a9c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 15 Dec 2021 07:33:49 +0900 Subject: [PATCH 348/419] Remove whitespace --- .../Visual/Playlists/TestScenePlaylistsRoomCreation.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index 9dd8b41dcd..a426f075e1 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -191,7 +191,6 @@ namespace osu.Game.Tests.Visual.Playlists } private void setupAndCreateRoom(Action room) - { AddStep("setup room", () => room(SelectedRoom.Value)); From e662a9f0c44bd5b9dab876f60a90919f5c53e44b Mon Sep 17 00:00:00 2001 From: rumoi Date: Wed, 15 Dec 2021 12:36:45 +1300 Subject: [PATCH 349/419] Remove redundant code. --- .../Difficulty/Skills/Speed.cs | 101 +++++++++--------- 1 file changed, 49 insertions(+), 52 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index d26808bf04..06d1ef7346 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -66,67 +66,64 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)Previous[i]; OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)Previous[i + 1]; - double currHistoricalDecay = Math.Max(0, (history_time_max - (current.StartTime - currObj.StartTime))) / history_time_max; // scales note 0 to 1 from history to now + double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now - if (currHistoricalDecay != 0) + currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count. + + double currDelta = currObj.StrainTime; + double prevDelta = prevObj.StrainTime; + double lastDelta = lastObj.StrainTime; + double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. + + double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6)); + + windowPenalty = Math.Min(1, windowPenalty); + + double effectiveRatio = windowPenalty * currRatio; + + if (firstDeltaSwitch) { - currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count. - - double currDelta = currObj.StrainTime; - double prevDelta = prevObj.StrainTime; - double lastDelta = lastObj.StrainTime; - double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. - - double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6)); - - windowPenalty = Math.Min(1, windowPenalty); - - double effectiveRatio = windowPenalty * currRatio; - - if (firstDeltaSwitch) + if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta)) { - if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta)) - { - if (islandSize < 7) - islandSize++; // island is still progressing, count size. - } - else - { - if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window - effectiveRatio *= 0.125; - - if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle - effectiveRatio *= 0.25; - - if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet) - effectiveRatio *= 0.25; - - if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5) - effectiveRatio *= 0.50; - - if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this. - effectiveRatio *= 0.125; - - rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2; - - startRatio = effectiveRatio; - - previousIslandSize = islandSize; // log the last island size. - - if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting - firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size. - - islandSize = 1; - } + if (islandSize < 7) + islandSize++; // island is still progressing, count size. } - else if (prevDelta > 1.25 * currDelta) // we want to be speeding up. + else { - // Begin counting island until we change speed again. - firstDeltaSwitch = true; + if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window + effectiveRatio *= 0.125; + + if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle + effectiveRatio *= 0.25; + + if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet) + effectiveRatio *= 0.25; + + if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5) + effectiveRatio *= 0.50; + + if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this. + effectiveRatio *= 0.125; + + rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2; + startRatio = effectiveRatio; + + previousIslandSize = islandSize; // log the last island size. + + if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting + firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size. + islandSize = 1; } } + else if (prevDelta > 1.25 * currDelta) // we want to be speeding up. + { + // Begin counting island until we change speed again. + firstDeltaSwitch = true; + startRatio = effectiveRatio; + islandSize = 1; + } } return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though) From 9d85beddbe5c45fa3985a6994f9dab05267eab9f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 11:16:37 +0900 Subject: [PATCH 350/419] Fix null reference in some tests due to missing realm context factory --- osu.Game/Skinning/Skin.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index ee92b6b40a..d606d94b97 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -43,7 +43,11 @@ namespace osu.Game.Skinning protected Skin(SkinInfo skin, IStorageResourceProvider resources, [CanBeNull] Stream configurationStream = null) { - SkinInfo = skin.ToLive(resources.RealmContextFactory); + SkinInfo = resources?.RealmContextFactory != null + ? skin.ToLive(resources.RealmContextFactory) + // This path should only be used in some tests. + : skin.ToLiveUnmanaged(); + this.resources = resources; configurationStream ??= getConfigurationStream(); From 0c11fe741309f8b990f4d85b69080dd9beb53f55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 12:45:09 +0900 Subject: [PATCH 351/419] Fix toast popups spamming samples when adjusting osu!mania scroll speed during gameplay Not the most robust of fixes, but as per the reasoning described in the issue thread, a proper fix will take considerably more effort. This intends to fix the issue first and foremost, as it sounds so bad I'd want to mute my sound before adjusting currently. Closes #15718. --- osu.Game/Overlays/OSD/TrackedSettingToast.cs | 24 ++++++++++++++++++++ osu.Game/Overlays/OnScreenDisplay.cs | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/OSD/TrackedSettingToast.cs b/osu.Game/Overlays/OSD/TrackedSettingToast.cs index 51214fe460..9939ba024e 100644 --- a/osu.Game/Overlays/OSD/TrackedSettingToast.cs +++ b/osu.Game/Overlays/OSD/TrackedSettingToast.cs @@ -5,12 +5,14 @@ using System; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Configuration.Tracking; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Game.Configuration; using osu.Game.Graphics; using osuTK; using osuTK.Graphics; @@ -28,6 +30,8 @@ namespace osu.Game.Overlays.OSD private Sample sampleOff; private Sample sampleChange; + private Bindable lastPlaybackTime; + public TrackedSettingToast(SettingDescription description) : base(description.Name, description.Value, description.Shortcut) { @@ -75,10 +79,28 @@ namespace osu.Game.Overlays.OSD optionLights.Add(new OptionLight { Glowing = i == selectedOption }); } + [Resolved] + private SessionStatics statics { get; set; } + protected override void LoadComplete() { base.LoadComplete(); + playSound(); + } + + private void playSound() + { + // This debounce code roughly follows what we're using in HoverSampleDebounceComponent. + // We're sharing the existing static for hover sounds because it doesn't really matter if they block each other. + // This is a simple solution, but if this ever becomes a problem (or other performance issues arise), + // the whole toast system should be rewritten to avoid recreating this drawable each time a value changes. + lastPlaybackTime = statics.GetBindable(Static.LastHoverSoundPlaybackTime); + + bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; + + if (!enoughTimePassedSinceLastPlayback) return; + if (optionCount == 1) { if (selectedOption == 0) @@ -93,6 +115,8 @@ namespace osu.Game.Overlays.OSD sampleChange.Frequency.Value = 1 + (double)selectedOption / (optionCount - 1) * 0.25f; sampleChange.Play(); } + + lastPlaybackTime.Value = Time.Current; } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/OnScreenDisplay.cs b/osu.Game/Overlays/OnScreenDisplay.cs index be9d3cd794..6b3696ced9 100644 --- a/osu.Game/Overlays/OnScreenDisplay.cs +++ b/osu.Game/Overlays/OnScreenDisplay.cs @@ -101,7 +101,7 @@ namespace osu.Game.Overlays DisplayTemporarily(box); }); - private void displayTrackedSettingChange(SettingDescription description) => Display(new TrackedSettingToast(description)); + private void displayTrackedSettingChange(SettingDescription description) => Scheduler.AddOnce(Display, new TrackedSettingToast(description)); private TransformSequence fadeIn; private ScheduledDelegate fadeOut; From 828072bceaad5cb10ae39abdec82148136707fa0 Mon Sep 17 00:00:00 2001 From: JamesTheGeek Date: Tue, 14 Dec 2021 23:23:11 -0500 Subject: [PATCH 352/419] Fix issue #15869 The taiko-slider is not included in `Playfield`, so it doesn't get hidden when calling `drawableRuleSet.Playfield.Hide()`. Calling `drawableRuleSet.Hide()` hides the taiko-slider, in addition to the rest of the `Playfield`. --- osu.Game/Rulesets/Mods/ModCinema.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index c78088ba2d..f28ef1edeb 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Mods drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods)); // AlwaysPresent required for hitsounds - drawableRuleset.Playfield.AlwaysPresent = true; - drawableRuleset.Playfield.Hide(); + drawableRuleset.AlwaysPresent = true; + drawableRuleset.Hide(); } } From b326ccc1968f5ff757ece4dd2806c878a2724a38 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Wed, 15 Dec 2021 07:13:24 +0100 Subject: [PATCH 353/419] Move logic to framework and update all usages --- osu.Game/Graphics/UserInterface/OsuNumberBox.cs | 4 +++- osu.Game/Overlays/Settings/SettingsNumberBox.cs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs index 36288c745a..3d565a4464 100644 --- a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs @@ -1,10 +1,12 @@ // 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.Extensions; + namespace osu.Game.Graphics.UserInterface { public class OsuNumberBox : OsuTextBox { - protected override bool CanAddCharacter(char character) => char.IsNumber(character); + protected override bool CanAddCharacter(char character) => character.IsAsciiDigit(); } } diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index 0a4949f8b6..cc4446033a 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -67,7 +68,7 @@ namespace osu.Game.Overlays.Settings private class OutlinedNumberBox : OutlinedTextBox { - protected override bool CanAddCharacter(char character) => character >= '0' && character <= '9'; + protected override bool CanAddCharacter(char character) => character.IsAsciiDigit(); public new void NotifyInputError() => base.NotifyInputError(); } From e9187cc3cf3192b3559b23b7c4cf5a60d4b1832a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 16:13:12 +0900 Subject: [PATCH 354/419] Add failing test showing expanded state being unexpectedly lost --- .../Visual/Beatmaps/TestSceneBeatmapCard.cs | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs index f835d21603..0b9857486a 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs @@ -6,12 +6,12 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Graphics.Containers; using osu.Game.Online.API; @@ -19,11 +19,10 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osuTK; -using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; namespace osu.Game.Tests.Visual.Beatmaps { - public class TestSceneBeatmapCard : OsuTestScene + public class TestSceneBeatmapCard : OsuManualInputManagerTestScene { /// /// All cards on this scene use a common online ID to ensure that map download, preview tracks, etc. can be tested manually with online sources. @@ -253,14 +252,32 @@ namespace osu.Game.Tests.Visual.Beatmaps public void TestNormal() { createTestCase(beatmapSetInfo => new BeatmapCard(beatmapSetInfo)); + } - AddToggleStep("toggle expanded state", expanded => - { - var card = this.ChildrenOfType().Last(); - if (!card.Expanded.Disabled) - card.Expanded.Value = expanded; - }); - AddToggleStep("disable/enable expansion", disabled => this.ChildrenOfType().ForEach(card => card.Expanded.Disabled = disabled)); + [Test] + public void TestHoverState() + { + AddStep("create cards", () => Child = createContent(OverlayColourScheme.Blue, s => new BeatmapCard(s))); + + AddStep("Hover card", () => InputManager.MoveMouseTo(firstCard())); + AddWaitStep("wait for potential state change", 5); + AddAssert("card is not expanded", () => !firstCard().Expanded.Value); + + AddStep("Hover spectrum display", () => InputManager.MoveMouseTo(firstCard().ChildrenOfType().Single())); + AddUntilStep("card is expanded", () => firstCard().Expanded.Value); + + AddStep("Hover difficulty content", () => InputManager.MoveMouseTo(firstCard().ChildrenOfType().Single())); + AddWaitStep("wait for potential state change", 5); + AddAssert("card is still expanded", () => firstCard().Expanded.Value); + + AddStep("Hover main content again", () => InputManager.MoveMouseTo(firstCard())); + AddWaitStep("wait for potential state change", 5); + AddAssert("card is still expanded", () => firstCard().Expanded.Value); + + AddStep("Hover away", () => InputManager.MoveMouseTo(this.ChildrenOfType().Last())); + AddUntilStep("card is not expanded", () => !firstCard().Expanded.Value); + + BeatmapCard firstCard() => this.ChildrenOfType().First(); } } } From 41e6c24dad8750edd2115c12ecd6857252036b46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 15:51:47 +0900 Subject: [PATCH 355/419] Expose `Expanded` state of `BeatmapCardContent` as read-only bindable This is just to reduce complexity of these interactions by ensuring that the expanded state can only be changed by the class itself. --- .../Beatmaps/Drawables/Cards/BeatmapCardContent.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs index 681f09c658..e353e61b71 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs @@ -31,7 +31,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards set => dropdownScroll.Child = value; } - public Bindable Expanded { get; } = new BindableBool(); + public IBindable Expanded => expanded; + + private readonly BindableBool expanded = new BindableBool(); private readonly Box background; private readonly Container content; @@ -128,7 +130,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards scheduledExpandedChange = Scheduler.AddDelayed(() => { if (!Expanded.Disabled) - Expanded.Value = true; + expanded.Value = true; }, 100); } @@ -141,7 +143,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards scheduledExpandedChange = Scheduler.AddDelayed(() => { if (!Expanded.Disabled) - Expanded.Value = false; + expanded.Value = false; }, 500); } @@ -154,7 +156,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards return; scheduledExpandedChange?.Cancel(); - Expanded.Value = false; + expanded.Value = false; } private void keep() @@ -163,7 +165,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards return; scheduledExpandedChange?.Cancel(); - Expanded.Value = true; + expanded.Value = true; } private void updateState() From ef4ab74565a843d23da2e05bc01a7ace7478a353 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 16:19:47 +0900 Subject: [PATCH 356/419] Also only expose `Expanded` state of `BeatmapCard` as read-only --- osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs | 6 ++++-- osu.Game/Screens/Play/SoloSpectator.cs | 5 +---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index d93ac841ab..435a8227bd 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards public const float TRANSITION_DURATION = 400; public const float CORNER_RADIUS = 10; - public Bindable Expanded { get; } = new BindableBool(); + public IBindable Expanded { get; } private const float width = 408; private const float height = 100; @@ -64,9 +64,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public BeatmapCard(APIBeatmapSet beatmapSet) + public BeatmapCard(APIBeatmapSet beatmapSet, bool allowExpansion = true) : base(HoverSampleSet.Submit) { + Expanded = new BindableBool { Disabled = !allowExpansion }; + this.beatmapSet = beatmapSet; favouriteState = new Bindable(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount)); downloadTracker = new BeatmapDownloadTracker(beatmapSet); diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index 3918dbe8fc..ba5663bfa3 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -228,10 +228,7 @@ namespace osu.Game.Screens.Play onlineBeatmapRequest.Success += beatmapSet => Schedule(() => { this.beatmapSet = beatmapSet; - beatmapPanelContainer.Child = new BeatmapCard(this.beatmapSet) - { - Expanded = { Disabled = true } - }; + beatmapPanelContainer.Child = new BeatmapCard(this.beatmapSet, allowExpansion: false); checkForAutomaticDownload(); }); From 7a9db22c5240a600b87a40b74cc6416cc0d9122f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 16:02:43 +0900 Subject: [PATCH 357/419] Tidy up method naming and structure for expanded state changes --- .../Beatmaps/Drawables/Cards/BeatmapCard.cs | 6 +-- .../Drawables/Cards/BeatmapCardContent.cs | 52 +++++-------------- 2 files changed, 15 insertions(+), 43 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 435a8227bd..2e39fafa05 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -284,7 +284,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Hovered = _ => { - content.ScheduleShow(); + content.ExpandAfterDelay(); return false; }, Unhovered = _ => @@ -292,7 +292,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards // This hide should only trigger if the expanded content has not shown yet. // ie. if the user has not shown intent to want to see it (quickly moved over the info row area). if (!Expanded.Value) - content.ScheduleHide(); + content.CollapseAfterDelay(); } } } @@ -368,7 +368,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards protected override void OnHoverLost(HoverLostEvent e) { - content.ScheduleHide(); + content.CollapseAfterDelay(); updateState(); base.OnHoverLost(e); diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs index e353e61b71..148372786a 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs @@ -56,7 +56,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards AutoSizeAxes = Axes.Y, CornerRadius = BeatmapCard.CORNER_RADIUS, Masking = true, - Unhovered = _ => checkForHide(), + Unhovered = _ => collapseIfNotHovered(), Children = new Drawable[] { background = new Box @@ -78,10 +78,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards Alpha = 0, Hovered = _ => { - keep(); + queueExpandedStateChange(true); return true; }, - Unhovered = _ => checkForHide(), + Unhovered = _ => collapseIfNotHovered(), Child = dropdownScroll = new ExpandedContentScrollContainer { RelativeSizeAxes = Axes.X, @@ -121,51 +121,23 @@ namespace osu.Game.Beatmaps.Drawables.Cards private ScheduledDelegate? scheduledExpandedChange; - public void ScheduleShow() - { - scheduledExpandedChange?.Cancel(); - if (Expanded.Disabled || Expanded.Value) - return; + public void ExpandAfterDelay() => queueExpandedStateChange(true, 100); - scheduledExpandedChange = Scheduler.AddDelayed(() => - { - if (!Expanded.Disabled) - expanded.Value = true; - }, 100); + public void CollapseAfterDelay() => queueExpandedStateChange(false, 500); + + private void collapseIfNotHovered() + { + if (!content.IsHovered && !dropdownContent.IsHovered) + queueExpandedStateChange(false); } - public void ScheduleHide() - { - scheduledExpandedChange?.Cancel(); - if (Expanded.Disabled || !Expanded.Value) - return; - - scheduledExpandedChange = Scheduler.AddDelayed(() => - { - if (!Expanded.Disabled) - expanded.Value = false; - }, 500); - } - - private void checkForHide() - { - if (Expanded.Disabled) - return; - - if (content.IsHovered || dropdownContent.IsHovered) - return; - - scheduledExpandedChange?.Cancel(); - expanded.Value = false; - } - - private void keep() + private void queueExpandedStateChange(bool newState, int delay = 0) { if (Expanded.Disabled) return; scheduledExpandedChange?.Cancel(); - expanded.Value = true; + scheduledExpandedChange = Scheduler.AddDelayed(() => expanded.Value = newState, delay); } private void updateState() From 94d1a2aacae0a6472e06eeb004004a984feb69c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 16:37:44 +0900 Subject: [PATCH 358/419] Remove unnecessary collapse call from `BeatmapCard` This is already handled at the `BeatmapCardContent` level. This call actually causes the buggy behaviour reported in https://github.com/ppy/osu/discussions/16085. --- osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 2e39fafa05..be7119be36 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -368,8 +368,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards protected override void OnHoverLost(HoverLostEvent e) { - content.CollapseAfterDelay(); - updateState(); base.OnHoverLost(e); } From 6a1f535257c49720f9f81ca80d9942d44889456d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 16:38:19 +0900 Subject: [PATCH 359/419] Refactor cancellation of expand to be more explicit --- osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs | 6 +++--- osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index be7119be36..1e24501426 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -289,10 +289,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards }, Unhovered = _ => { - // This hide should only trigger if the expanded content has not shown yet. - // ie. if the user has not shown intent to want to see it (quickly moved over the info row area). + // Handles the case where a user has not shown explicit intent to view expanded info. + // ie. quickly moved over the info row area but didn't remain within it. if (!Expanded.Value) - content.CollapseAfterDelay(); + content.CancelExpand(); } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs index 148372786a..0739f7328f 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs @@ -123,7 +123,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards public void ExpandAfterDelay() => queueExpandedStateChange(true, 100); - public void CollapseAfterDelay() => queueExpandedStateChange(false, 500); + public void CancelExpand() => scheduledExpandedChange?.Cancel(); private void collapseIfNotHovered() { From ad430a627701afa2fcd7c538b99e232d4f54a693 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 16:44:58 +0900 Subject: [PATCH 360/419] Centralise hover state handling (and fix back-to-front conditionals) --- .../Beatmaps/Drawables/Cards/BeatmapCardContent.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs index 0739f7328f..286e03e700 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs @@ -56,7 +56,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards AutoSizeAxes = Axes.Y, CornerRadius = BeatmapCard.CORNER_RADIUS, Masking = true, - Unhovered = _ => collapseIfNotHovered(), + Unhovered = _ => updateFromHoverChange(), Children = new Drawable[] { background = new Box @@ -78,10 +78,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards Alpha = 0, Hovered = _ => { - queueExpandedStateChange(true); + updateFromHoverChange(); return true; }, - Unhovered = _ => collapseIfNotHovered(), + Unhovered = _ => updateFromHoverChange(), Child = dropdownScroll = new ExpandedContentScrollContainer { RelativeSizeAxes = Axes.X, @@ -125,11 +125,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards public void CancelExpand() => scheduledExpandedChange?.Cancel(); - private void collapseIfNotHovered() - { - if (!content.IsHovered && !dropdownContent.IsHovered) - queueExpandedStateChange(false); - } + private void updateFromHoverChange() => + queueExpandedStateChange(content.IsHovered || dropdownContent.IsHovered, 100); private void queueExpandedStateChange(bool newState, int delay = 0) { From 42f14667a360a16e95197cd2d2635bb9e4026479 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 16:50:55 +0900 Subject: [PATCH 361/419] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 0c922c09ac..5e5e110dfb 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index adb25f46fe..bc429f9af1 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index db5d9af865..1ae9aa3dc5 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From 740a6f16c79a606652649fc9e71cb607bfb343d9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 15 Dec 2021 16:52:50 +0900 Subject: [PATCH 362/419] Fix exception when updating the room's visual playlist --- .../Multiplayer/TestSceneMultiplayer.cs | 43 +++++++++++++++++-- .../Match/Playlist/MultiplayerPlaylist.cs | 6 ++- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index bc2902480d..2ea7f2541f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -30,6 +30,7 @@ using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Spectate; @@ -594,9 +595,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - pressReadyButton(); - pressReadyButton(); - AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player); + enterGameplay(); // Gameplay runs in real-time, so we need to incrementally check if gameplay has finished in order to not time out. for (double i = 1000; i < TestResources.QUICK_BEATMAP_LENGTH; i += 1000) @@ -656,6 +655,44 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } + [Test] + public void TestItemAddedAndDeletedByOtherUserDuringGameplay() + { + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + QueueMode = { Value = QueueMode.AllPlayers }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + }); + + enterGameplay(); + + AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); + AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem + { + BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID ?? -1 + }))); + AddStep("delete item as other user", () => client.RemoveUserPlaylistItem(1234, 2)); + + AddUntilStep("wait for item to be deleted", () => client.Room?.Playlist.Count == 1); + AddStep("exit gameplay as initial user", () => multiplayerScreenStack.MultiplayerScreen.MakeCurrent()); + AddUntilStep("queue is empty", () => this.ChildrenOfType().Single().Items.Count == 0); + } + + private void enterGameplay() + { + pressReadyButton(); + pressReadyButton(); + AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player); + } + private ReadyButton readyButton => this.ChildrenOfType().Single(); private void pressReadyButton() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index 4971489769..7b90532cce 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -121,7 +121,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist private void addItemToLists(MultiplayerPlaylistItem item) { - var apiItem = Playlist.Single(i => i.ID == item.ID); + var apiItem = Playlist.SingleOrDefault(i => i.ID == item.ID); + + // Item could have been removed from the playlist while the local player was in gameplay. + if (apiItem == null) + return; if (item.Expired) historyList.Items.Add(apiItem); From 694ee687250b3c910154ab2d83a927d6c061bdf3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 16:59:33 +0900 Subject: [PATCH 363/419] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 5e5e110dfb..1131203a95 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index bc429f9af1..5a0c999fb0 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 1ae9aa3dc5..27ac1bf647 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + From d22e1b90010ea570f03f4aad8450f72bce2d02e9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 15 Dec 2021 16:59:34 +0900 Subject: [PATCH 364/419] Add another until step to guard against async test issues --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 2ea7f2541f..1d951c257c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -679,9 +679,12 @@ namespace osu.Game.Tests.Visual.Multiplayer { BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID ?? -1 }))); - AddStep("delete item as other user", () => client.RemoveUserPlaylistItem(1234, 2)); - AddUntilStep("wait for item to be deleted", () => client.Room?.Playlist.Count == 1); + AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); + + AddStep("delete item as other user", () => client.RemoveUserPlaylistItem(1234, 2)); + AddUntilStep("item removed from playlist", () => client.Room?.Playlist.Count == 1); + AddStep("exit gameplay as initial user", () => multiplayerScreenStack.MultiplayerScreen.MakeCurrent()); AddUntilStep("queue is empty", () => this.ChildrenOfType().Single().Items.Count == 0); } From 39a0a2113219ef3319da9dd4e6e0c023b5f0ce52 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 17:30:09 +0900 Subject: [PATCH 365/419] Add test coverage of same scenario without deletion --- .../Multiplayer/TestSceneMultiplayer.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 1d951c257c..ed8d50589f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -655,6 +655,37 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } + [Test] + public void TestItemAddedByOtherUserDuringGameplay() + { + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + QueueMode = { Value = QueueMode.AllPlayers }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + }); + + enterGameplay(); + + AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); + AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem + { + BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID ?? -1 + }))); + + AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); + + AddStep("exit gameplay as initial user", () => multiplayerScreenStack.MultiplayerScreen.MakeCurrent()); + AddUntilStep("queue contains item", () => this.ChildrenOfType().Single().Items.Single().ID == 2); + } + [Test] public void TestItemAddedAndDeletedByOtherUserDuringGameplay() { From 18d7b7920798bec5097f30b1ff85f413a4adf098 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 15 Dec 2021 17:37:39 +0900 Subject: [PATCH 366/419] Don't reset spectating state if gameplay is finished --- .../Multiplayer/TestSceneMultiplayer.cs | 91 ++++++++++++++++++- .../Visual/TestMultiplayerScreenStack.cs | 2 +- .../Spectate/MultiSpectatorScreen.cs | 9 +- 3 files changed, 96 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index bc2902480d..a2de391aec 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -30,6 +30,7 @@ using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; +using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Spectate; @@ -656,23 +657,107 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } + [Test] + public void TestSpectatingStateResetOnBackButtonDuringGameplay() + { + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + QueueMode = { Value = QueueMode.AllPlayers }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + }); + + AddStep("set spectating state", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); + AddUntilStep("state set to spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + + AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); + AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready)); + + pressReadyButton(1234); + AddUntilStep("wait for gameplay", () => (multiplayerScreenStack.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true); + + AddStep("press back button and exit", () => + { + multiplayerScreenStack.OnBackButton(); + multiplayerScreenStack.Exit(); + }); + + AddUntilStep("wait for return to match subscreen", () => multiplayerScreenStack.MultiplayerScreen.IsCurrentScreen()); + AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle); + } + + [Test] + public void TestSpectatingStateNotResetOnBackButtonOutsideOfGameplay() + { + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + QueueMode = { Value = QueueMode.AllPlayers }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + }); + + AddStep("set spectating state", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); + AddUntilStep("state set to spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + + AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); + AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready)); + + pressReadyButton(1234); + AddUntilStep("wait for gameplay", () => (multiplayerScreenStack.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true); + AddStep("set other user loaded", () => client.ChangeUserState(1234, MultiplayerUserState.Loaded)); + AddStep("set other user finished play", () => client.ChangeUserState(1234, MultiplayerUserState.FinishedPlay)); + + AddStep("press back button and exit", () => + { + multiplayerScreenStack.OnBackButton(); + multiplayerScreenStack.Exit(); + }); + + AddUntilStep("wait for return to match subscreen", () => multiplayerScreenStack.MultiplayerScreen.IsCurrentScreen()); + AddWaitStep("wait for possible state change", 5); + AddUntilStep("user state is spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + } + + private void enterGameplay() + { + pressReadyButton(); + pressReadyButton(); + AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player); + } + private ReadyButton readyButton => this.ChildrenOfType().Single(); - private void pressReadyButton() + private void pressReadyButton(int? playingUserId = null) { AddUntilStep("wait for ready button to be enabled", () => readyButton.Enabled.Value); MultiplayerUserState lastState = MultiplayerUserState.Idle; + MultiplayerRoomUser user = null; AddStep("click ready button", () => { - lastState = client.LocalUser?.State ?? MultiplayerUserState.Idle; + user = playingUserId == null ? client.LocalUser : client.Room?.Users.Single(u => u.UserID == playingUserId); + lastState = user?.State ?? MultiplayerUserState.Idle; InputManager.MoveMouseTo(readyButton); InputManager.Click(MouseButton.Left); }); - AddUntilStep("wait for state change", () => client.LocalUser?.State != lastState); + AddUntilStep("wait for state change", () => user?.State != lastState); } private void createRoom(Func room) diff --git a/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs b/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs index 7f1171db1f..370f3bd0ae 100644 --- a/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs +++ b/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual ((DummyAPIAccess)api).HandleRequest = request => multiplayerScreen.RequestsHandler.HandleRequest(request, api.LocalUser.Value, game); } - public override bool OnBackButton() => multiplayerScreen.OnBackButton(); + public override bool OnBackButton() => (screenStack.CurrentScreen as OsuScreen)?.OnBackButton() ?? base.OnBackButton(); public override bool OnExiting(IScreen next) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 9ac64add9a..7350408eba 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -226,8 +227,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public override bool OnBackButton() { - // On a manual exit, set the player state back to idle. - multiplayerClient.ChangeState(MultiplayerUserState.Idle); + Debug.Assert(multiplayerClient.Room != null); + + // On a manual exit, set the player back to idle unless gameplay has finished. + if (multiplayerClient.Room.State != MultiplayerRoomState.Open) + multiplayerClient.ChangeState(MultiplayerUserState.Idle); + return base.OnBackButton(); } } From c50206e25e285994df75c1927d5aa2889ce9d100 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Dec 2021 14:11:38 +0900 Subject: [PATCH 367/419] Update a few more public facing usages of "lazer" --- CONTRIBUTING.md | 8 ++++---- README.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e14be20642..ae2bdd2e82 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing Guidelines -Thank you for showing interest in the development of osu!lazer! We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience. +Thank you for showing interest in the development of osu!. We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience. These are not "official rules" *per se*, but following them will help everyone deal with things in the most efficient manner. @@ -32,7 +32,7 @@ Issues, bug reports and feature suggestions are welcomed, though please keep in * **Provide more information when asked to do so.** - Sometimes when a bug is more elusive or complicated, none of the information listed above will pinpoint a concrete cause of the problem. In this case we will most likely ask you for additional info, such as a Windows Event Log dump or a copy of your local lazer database (`client.db`). Providing that information is beneficial to both parties - we can track down the problem better, and hopefully fix it for you at some point once we know where it is! + Sometimes when a bug is more elusive or complicated, none of the information listed above will pinpoint a concrete cause of the problem. In this case we will most likely ask you for additional info, such as a Windows Event Log dump or a copy of your local osu! database (`client.db`). Providing that information is beneficial to both parties - we can track down the problem better, and hopefully fix it for you at some point once we know where it is! * **When submitting a feature proposal, please describe it in the most understandable way you can.** @@ -54,7 +54,7 @@ Issues, bug reports and feature suggestions are welcomed, though please keep in We also welcome pull requests from unaffiliated contributors. The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues that you can work on; we also mark issues that we think would be good for newcomers with the [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label. -However, do keep in mind that the core team is committed to bringing osu!lazer up to par with stable first and foremost, so depending on what your contribution concerns, it might not be merged and released right away. Our approach to managing issues and their priorities is described [in the wiki](https://github.com/ppy/osu/wiki/Project-management). +However, do keep in mind that the core team is committed to bringing osu!(lazer) up to par with osu!(stable) first and foremost, so depending on what your contribution concerns, it might not be merged and released right away. Our approach to managing issues and their priorities is described [in the wiki](https://github.com/ppy/osu/wiki/Project-management). Here are some key things to note before jumping in: @@ -128,7 +128,7 @@ Here are some key things to note before jumping in: * **Don't mistake criticism of code for criticism of your person.** - As mentioned before, we are highly committed to quality when it comes to the lazer project. This means that contributions from less experienced community members can take multiple rounds of review to get to a mergeable state. We try our utmost best to never conflate a person with the code they authored, and to keep the discussion focused on the code at all times. Please consider our comments and requests a learning experience, and don't treat it as a personal attack. + As mentioned before, we are highly committed to quality when it comes to the osu! project. This means that contributions from less experienced community members can take multiple rounds of review to get to a mergeable state. We try our utmost best to never conflate a person with the code they authored, and to keep the discussion focused on the code at all times. Please consider our comments and requests a learning experience, and don't treat it as a personal attack. * **Feel free to reach out for help.** diff --git a/README.md b/README.md index 786ce2589d..24b70b2de6 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ A free-to-win rhythm game. Rhythm is just a *click* away! -The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the codename "*lazer*". As in sharper than cutting-edge. +The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge. ## Status From b3e83a47a4651442ddd499d66f63a7b96c6173bf Mon Sep 17 00:00:00 2001 From: Imad Dodin Date: Wed, 15 Dec 2021 21:34:59 -0800 Subject: [PATCH 368/419] Convert to Local Time in Date Tooltip --- osu.Game/Graphics/DateTooltip.cs | 6 ++++-- osu.Game/Graphics/DrawableDate.cs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/DateTooltip.cs b/osu.Game/Graphics/DateTooltip.cs index 3094f9cc2b..d5768b259a 100644 --- a/osu.Game/Graphics/DateTooltip.cs +++ b/osu.Game/Graphics/DateTooltip.cs @@ -65,8 +65,10 @@ namespace osu.Game.Graphics public void SetContent(DateTimeOffset date) { - dateText.Text = $"{date:d MMMM yyyy} "; - timeText.Text = $"{date:HH:mm:ss \"UTC\"z}"; + DateTimeOffset localDate = date.ToLocalTime(); + + dateText.Text = $"{localDate:d MMMM yyyy} "; + timeText.Text = $"{localDate:HH:mm:ss \"UTC\"z}"; } public void Move(Vector2 pos) => Position = pos; diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index 567a39b4f4..4605976692 100644 --- a/osu.Game/Graphics/DrawableDate.cs +++ b/osu.Game/Graphics/DrawableDate.cs @@ -22,7 +22,7 @@ namespace osu.Game.Graphics if (date == value) return; - date = value.ToLocalTime(); + date = value; if (LoadState >= LoadState.Ready) updateTime(); From a9dbcd92a1fc05a8bbb800f1f0c5a4c7c2f147f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Dec 2021 15:11:48 +0900 Subject: [PATCH 369/419] Split out unmanaged implementation of `RealmLive` into its own class --- osu.Game/Database/RealmLive.cs | 16 ++------ osu.Game/Database/RealmLiveUnmanaged.cs | 44 ++++++++++++++++++++++ osu.Game/Database/RealmObjectExtensions.cs | 4 +- 3 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 osu.Game/Database/RealmLiveUnmanaged.cs diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 4f7bdf93e4..90b8814c24 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -24,20 +24,16 @@ namespace osu.Game.Database /// private readonly T data; - private readonly RealmContextFactory? realmFactory; + private readonly RealmContextFactory realmFactory; /// /// Construct a new instance of live realm data. /// /// The realm data. /// The realm factory the data was sourced from. May be null for an unmanaged object. - public RealmLive(T data, RealmContextFactory? realmFactory) + public RealmLive(T data, RealmContextFactory realmFactory) { this.data = data; - - if (IsManaged && realmFactory == null) - throw new ArgumentException(@"Realm factory must be provided for a managed instance", nameof(realmFactory)); - this.realmFactory = realmFactory; ID = data.ID; @@ -55,9 +51,6 @@ namespace osu.Game.Database return; } - if (realmFactory == null) - throw new ArgumentException(@"Realm factory must be provided for a managed instance", nameof(realmFactory)); - using (var realm = realmFactory.CreateContext()) perform(realm.Find(ID)); } @@ -74,9 +67,6 @@ namespace osu.Game.Database if (!IsManaged) return perform(data); - if (realmFactory == null) - throw new ArgumentException(@"Realm factory must be provided for a managed instance", nameof(realmFactory)); - using (var realm = realmFactory.CreateContext()) return perform(realm.Find(ID)); } @@ -108,7 +98,7 @@ namespace osu.Game.Database if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException($"Can't use {nameof(Value)} on managed objects from non-update threads"); - return realmFactory!.Context.Find(ID); + return realmFactory.Context.Find(ID); } } diff --git a/osu.Game/Database/RealmLiveUnmanaged.cs b/osu.Game/Database/RealmLiveUnmanaged.cs new file mode 100644 index 0000000000..5a69898206 --- /dev/null +++ b/osu.Game/Database/RealmLiveUnmanaged.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using Realms; + +#nullable enable + +namespace osu.Game.Database +{ + /// + /// Provides a method of working with unmanaged realm objects. + /// Usually used for testing purposes where the instance is never required to be managed. + /// + /// The underlying object type. + public class RealmLiveUnmanaged : ILive where T : RealmObjectBase, IHasGuidPrimaryKey + { + /// + /// Construct a new instance of live realm data. + /// + /// The realm data. + public RealmLiveUnmanaged(T data) + { + Value = data; + } + + public bool Equals(ILive? other) => ID == other?.ID; + + public Guid ID => Value.ID; + + public void PerformRead(Action perform) => perform(Value); + + public TReturn PerformRead(Func perform) => perform(Value); + + public void PerformWrite(Action perform) => throw new InvalidOperationException(@"Can't perform writes on a non-managed underlying value"); + + public bool IsManaged => false; + + /// + /// The original live data used to create this instance. + /// + public T Value { get; } + } +} diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index c546a70fae..e5177823ba 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -56,13 +56,13 @@ namespace osu.Game.Database public static List> ToLiveUnmanaged(this IEnumerable realmList) where T : RealmObject, IHasGuidPrimaryKey { - return realmList.Select(l => new RealmLive(l, null)).Cast>().ToList(); + return realmList.Select(l => new RealmLiveUnmanaged(l)).Cast>().ToList(); } public static ILive ToLiveUnmanaged(this T realmObject) where T : RealmObject, IHasGuidPrimaryKey { - return new RealmLive(realmObject, null); + return new RealmLiveUnmanaged(realmObject); } public static List> ToLive(this IEnumerable realmList, RealmContextFactory realmContextFactory) From 488374b4a27c56b0f26061d55e3f7648236e3354 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Dec 2021 16:41:47 +0900 Subject: [PATCH 370/419] Don't show multiplayer channels in chat overlay --- .../Visual/Online/TestSceneChatOverlay.cs | 19 +++++++++++++++++++ osu.Game/Overlays/ChatOverlay.cs | 14 +++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 9c65b2dc51..14f32df653 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -393,6 +393,25 @@ namespace osu.Game.Tests.Visual.Online channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single().Username == "some body"); } + [Test] + public void TestMultiplayerChannelIsNotShown() + { + Channel multiplayerChannel = null; + + AddStep("join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser()) + { + Name = "#mp_1", + Type = ChannelType.Multiplayer, + })); + + AddAssert("channel joined", () => channelManager.JoinedChannels.Contains(multiplayerChannel)); + AddAssert("channel not present in overlay", () => !chatOverlay.TabMap.ContainsKey(multiplayerChannel)); + AddAssert("multiplayer channel is not current", () => channelManager.CurrentChannel.Value != multiplayerChannel); + + AddStep("leave channel", () => channelManager.LeaveChannel(multiplayerChannel)); + AddAssert("channel left", () => !channelManager.JoinedChannels.Contains(multiplayerChannel)); + } + private void pressChannelHotkey(int number) { var channelKey = Key.Number0 + number; diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index cc3ce63bf7..72473d5750 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -237,10 +237,7 @@ namespace osu.Game.Overlays Schedule(() => { // TODO: consider scheduling bindable callbacks to not perform when overlay is not present. - channelManager.JoinedChannels.CollectionChanged += joinedChannelsChanged; - - foreach (Channel channel in channelManager.JoinedChannels) - ChannelTabControl.AddChannel(channel); + channelManager.JoinedChannels.BindCollectionChanged(joinedChannelsChanged, true); channelManager.AvailableChannels.CollectionChanged += availableChannelsChanged; availableChannelsChanged(null, null); @@ -436,12 +433,19 @@ namespace osu.Game.Overlays { case NotifyCollectionChangedAction.Add: foreach (Channel channel in args.NewItems.Cast()) - ChannelTabControl.AddChannel(channel); + { + if (channel.Type != ChannelType.Multiplayer) + ChannelTabControl.AddChannel(channel); + } + break; case NotifyCollectionChangedAction.Remove: foreach (Channel channel in args.OldItems.Cast()) { + if (!ChannelTabControl.Items.Contains(channel)) + continue; + ChannelTabControl.RemoveChannel(channel); var loaded = loadedChannels.Find(c => c.Channel == channel); From 0eac655cff09722b5077ff48cc09702d541c0326 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Dec 2021 18:21:48 +0900 Subject: [PATCH 371/419] Remove local screen change logging --- osu.Game/OsuGame.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a35191613c..d2d6dad0c6 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1149,16 +1149,11 @@ namespace osu.Game } } - private void screenPushed(IScreen lastScreen, IScreen newScreen) - { - ScreenChanged(lastScreen, newScreen); - Logger.Log($"Screen changed → {newScreen}"); - } + private void screenPushed(IScreen lastScreen, IScreen newScreen) => ScreenChanged(lastScreen, newScreen); private void screenExited(IScreen lastScreen, IScreen newScreen) { ScreenChanged(lastScreen, newScreen); - Logger.Log($"Screen changed ← {newScreen}"); if (newScreen == null) Exit(); From 434aa0367f4c58349bbc05258a02a99d233ef54f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Dec 2021 18:25:28 +0900 Subject: [PATCH 372/419] Add back `.ToLocalTime()` call to `DrawableDate` This is required because the class is used in many other places that don't locally call it. --- osu.Game/Graphics/DrawableDate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index 4605976692..567a39b4f4 100644 --- a/osu.Game/Graphics/DrawableDate.cs +++ b/osu.Game/Graphics/DrawableDate.cs @@ -22,7 +22,7 @@ namespace osu.Game.Graphics if (date == value) return; - date = value; + date = value.ToLocalTime(); if (LoadState >= LoadState.Ready) updateTime(); From 5ea081e89908e35993d29553218fe2254bda2c7b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Dec 2021 19:04:42 +0900 Subject: [PATCH 373/419] Test hyperdash generation in catch conversion tests --- .../CatchBeatmapConversionTest.cs | 12 +- .../Beatmaps/basic-expected-conversion.json | 921 ++++++++++++------ ...ock-repeat-slider-expected-conversion.json | 108 +- .../hardrock-spinner-expected-conversion.json | 51 +- .../hardrock-stream-expected-conversion.json | 99 +- ...t-bound-hr-offset-expected-conversion.json | 6 +- .../Beatmaps/slider-expected-conversion.json | 72 +- ...inner-and-circles-expected-conversion.json | 30 +- .../Beatmaps/spinner-expected-conversion.json | 51 +- 9 files changed, 926 insertions(+), 424 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs index 33fdcdaf1e..3352982c20 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs @@ -70,6 +70,7 @@ namespace osu.Game.Rulesets.Catch.Tests HitObject = hitObject; startTime = 0; position = 0; + hyperDash = false; } private double startTime; @@ -88,8 +89,17 @@ namespace osu.Game.Rulesets.Catch.Tests set => position = value; } + private bool hyperDash; + + public bool HyperDash + { + get => (HitObject as PalpableCatchHitObject)?.HyperDash ?? hyperDash; + set => hyperDash = value; + } + public bool Equals(ConvertValue other) => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience) - && Precision.AlmostEquals(Position, other.Position, conversion_lenience); + && Precision.AlmostEquals(Position, other.Position, conversion_lenience) + && HyperDash == other.hyperDash; } } diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json index b65d54a565..07ceb199bd 100644 --- a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json @@ -3,135 +3,168 @@ "StartTime": 500, "Objects": [{ "StartTime": 500, - "Position": 96 + "Position": 96, + "HyperDash": false }, { "StartTime": 562, - "Position": 100.84 + "Position": 100.84, + "HyperDash": false }, { "StartTime": 625, - "Position": 125 + "Position": 125, + "HyperDash": false }, { "StartTime": 687, - "Position": 152.84 + "Position": 152.84, + "HyperDash": false }, { "StartTime": 750, - "Position": 191 + "Position": 191, + "HyperDash": false }, { "StartTime": 812, - "Position": 212.84 + "Position": 212.84, + "HyperDash": false }, { "StartTime": 875, - "Position": 217 + "Position": 217, + "HyperDash": false }, { "StartTime": 937, - "Position": 234.84 + "Position": 234.84, + "HyperDash": false }, { "StartTime": 1000, - "Position": 256 + "Position": 256, + "HyperDash": false }, { "StartTime": 1062, - "Position": 267.84 + "Position": 267.84, + "HyperDash": false }, { "StartTime": 1125, - "Position": 284 + "Position": 284, + "HyperDash": false }, { "StartTime": 1187, - "Position": 311.84 + "Position": 311.84, + "HyperDash": false }, { "StartTime": 1250, - "Position": 350 + "Position": 350, + "HyperDash": false }, { "StartTime": 1312, - "Position": 359.84 + "Position": 359.84, + "HyperDash": false }, { "StartTime": 1375, - "Position": 367 + "Position": 367, + "HyperDash": false }, { "StartTime": 1437, - "Position": 400.84 + "Position": 400.84, + "HyperDash": false }, { "StartTime": 1500, - "Position": 416 + "Position": 416, + "HyperDash": false }, { "StartTime": 1562, - "Position": 377.159973 + "Position": 377.159973, + "HyperDash": false }, { "StartTime": 1625, - "Position": 367 + "Position": 367, + "HyperDash": false }, { "StartTime": 1687, - "Position": 374.159973 + "Position": 374.159973, + "HyperDash": false }, { "StartTime": 1750, - "Position": 353 + "Position": 353, + "HyperDash": false }, { "StartTime": 1812, - "Position": 329.159973 + "Position": 329.159973, + "HyperDash": false }, { "StartTime": 1875, - "Position": 288 + "Position": 288, + "HyperDash": false }, { "StartTime": 1937, - "Position": 259.159973 + "Position": 259.159973, + "HyperDash": false }, { "StartTime": 2000, - "Position": 256 + "Position": 256, + "HyperDash": false }, { "StartTime": 2058, - "Position": 232.44 + "Position": 232.44, + "HyperDash": false }, { "StartTime": 2116, - "Position": 222.879974 + "Position": 222.879974, + "HyperDash": false }, { "StartTime": 2174, - "Position": 185.319992 + "Position": 185.319992, + "HyperDash": false }, { "StartTime": 2232, - "Position": 177.76001 + "Position": 177.76001, + "HyperDash": false }, { "StartTime": 2290, - "Position": 162.200012 + "Position": 162.200012, + "HyperDash": false }, { "StartTime": 2348, - "Position": 158.639984 + "Position": 158.639984, + "HyperDash": false }, { "StartTime": 2406, - "Position": 111.079994 + "Position": 111.079994, + "HyperDash": false }, { "StartTime": 2500, - "Position": 96 + "Position": 96, + "HyperDash": false } ] }, @@ -139,71 +172,88 @@ "StartTime": 3000, "Objects": [{ "StartTime": 3000, - "Position": 18 + "Position": 18, + "HyperDash": false }, { "StartTime": 3062, - "Position": 249 + "Position": 249, + "HyperDash": false }, { "StartTime": 3125, - "Position": 184 + "Position": 184, + "HyperDash": false }, { "StartTime": 3187, - "Position": 477 + "Position": 477, + "HyperDash": false }, { "StartTime": 3250, - "Position": 43 + "Position": 43, + "HyperDash": false }, { "StartTime": 3312, - "Position": 494 + "Position": 494, + "HyperDash": false }, { "StartTime": 3375, - "Position": 135 + "Position": 135, + "HyperDash": false }, { "StartTime": 3437, - "Position": 30 + "Position": 30, + "HyperDash": false }, { "StartTime": 3500, - "Position": 11 + "Position": 11, + "HyperDash": false }, { "StartTime": 3562, - "Position": 239 + "Position": 239, + "HyperDash": false }, { "StartTime": 3625, - "Position": 505 + "Position": 505, + "HyperDash": false }, { "StartTime": 3687, - "Position": 353 + "Position": 353, + "HyperDash": false }, { "StartTime": 3750, - "Position": 136 + "Position": 136, + "HyperDash": false }, { "StartTime": 3812, - "Position": 135 + "Position": 135, + "HyperDash": false }, { "StartTime": 3875, - "Position": 346 + "Position": 346, + "HyperDash": false }, { "StartTime": 3937, - "Position": 39 + "Position": 39, + "HyperDash": false }, { "StartTime": 4000, - "Position": 300 + "Position": 300, + "HyperDash": false } ] }, @@ -211,71 +261,88 @@ "StartTime": 4500, "Objects": [{ "StartTime": 4500, - "Position": 398 + "Position": 398, + "HyperDash": false }, { "StartTime": 4562, - "Position": 151 + "Position": 151, + "HyperDash": false }, { "StartTime": 4625, - "Position": 73 + "Position": 73, + "HyperDash": false }, { "StartTime": 4687, - "Position": 311 + "Position": 311, + "HyperDash": false }, { "StartTime": 4750, - "Position": 90 + "Position": 90, + "HyperDash": false }, { "StartTime": 4812, - "Position": 264 + "Position": 264, + "HyperDash": false }, { "StartTime": 4875, - "Position": 477 + "Position": 477, + "HyperDash": false }, { "StartTime": 4937, - "Position": 473 + "Position": 473, + "HyperDash": false }, { "StartTime": 5000, - "Position": 120 + "Position": 120, + "HyperDash": false }, { "StartTime": 5062, - "Position": 115 + "Position": 115, + "HyperDash": false }, { "StartTime": 5125, - "Position": 163 + "Position": 163, + "HyperDash": false }, { "StartTime": 5187, - "Position": 447 + "Position": 447, + "HyperDash": false }, { "StartTime": 5250, - "Position": 72 + "Position": 72, + "HyperDash": false }, { "StartTime": 5312, - "Position": 257 + "Position": 257, + "HyperDash": false }, { "StartTime": 5375, - "Position": 153 + "Position": 153, + "HyperDash": false }, { "StartTime": 5437, - "Position": 388 + "Position": 388, + "HyperDash": false }, { "StartTime": 5500, - "Position": 336 + "Position": 336, + "HyperDash": false } ] }, @@ -283,39 +350,48 @@ "StartTime": 6000, "Objects": [{ "StartTime": 6000, - "Position": 13 + "Position": 13, + "HyperDash": false }, { "StartTime": 6062, - "Position": 429 + "Position": 429, + "HyperDash": false }, { "StartTime": 6125, - "Position": 381 + "Position": 381, + "HyperDash": false }, { "StartTime": 6187, - "Position": 186 + "Position": 186, + "HyperDash": false }, { "StartTime": 6250, - "Position": 267 + "Position": 267, + "HyperDash": false }, { "StartTime": 6312, - "Position": 305 + "Position": 305, + "HyperDash": false }, { "StartTime": 6375, - "Position": 456 + "Position": 456, + "HyperDash": false }, { "StartTime": 6437, - "Position": 26 + "Position": 26, + "HyperDash": false }, { "StartTime": 6500, - "Position": 238 + "Position": 238, + "HyperDash": false } ] }, @@ -323,71 +399,88 @@ "StartTime": 7000, "Objects": [{ "StartTime": 7000, - "Position": 256 + "Position": 256, + "HyperDash": false }, { "StartTime": 7062, - "Position": 262.84 + "Position": 262.84, + "HyperDash": false }, { "StartTime": 7125, - "Position": 295 + "Position": 295, + "HyperDash": false }, { "StartTime": 7187, - "Position": 303.84 + "Position": 303.84, + "HyperDash": false }, { "StartTime": 7250, - "Position": 336 + "Position": 336, + "HyperDash": false }, { "StartTime": 7312, - "Position": 319.16 + "Position": 319.16, + "HyperDash": false }, { "StartTime": 7375, - "Position": 306 + "Position": 306, + "HyperDash": false }, { "StartTime": 7437, - "Position": 272.16 + "Position": 272.16, + "HyperDash": false }, { "StartTime": 7500, - "Position": 256 + "Position": 256, + "HyperDash": false }, { "StartTime": 7562, - "Position": 255.84 + "Position": 255.84, + "HyperDash": false }, { "StartTime": 7625, - "Position": 300 + "Position": 300, + "HyperDash": false }, { "StartTime": 7687, - "Position": 320.84 + "Position": 320.84, + "HyperDash": false }, { "StartTime": 7750, - "Position": 336 + "Position": 336, + "HyperDash": false }, { "StartTime": 7803, - "Position": 319.04 + "Position": 319.04, + "HyperDash": false }, { "StartTime": 7857, - "Position": 283.76 + "Position": 283.76, + "HyperDash": false }, { "StartTime": 7910, - "Position": 265.8 + "Position": 265.8, + "HyperDash": false }, { "StartTime": 8000, - "Position": 256 + "Position": 256, + "HyperDash": false } ] }, @@ -395,167 +488,208 @@ "StartTime": 8500, "Objects": [{ "StartTime": 8500, - "Position": 32 + "Position": 32, + "HyperDash": false }, { "StartTime": 8562, - "Position": 21.8515015 + "Position": 21.8515015, + "HyperDash": false }, { "StartTime": 8625, - "Position": 44.5659637 + "Position": 44.5659637, + "HyperDash": false }, { "StartTime": 8687, - "Position": 33.3433228 + "Position": 33.3433228, + "HyperDash": false }, { "StartTime": 8750, - "Position": 63.58974 + "Position": 63.58974, + "HyperDash": false }, { "StartTime": 8812, - "Position": 71.23422 + "Position": 71.23422, + "HyperDash": false }, { "StartTime": 8875, - "Position": 62.7117844 + "Position": 62.7117844, + "HyperDash": false }, { "StartTime": 8937, - "Position": 65.52607 + "Position": 65.52607, + "HyperDash": false }, { "StartTime": 9000, - "Position": 101.81015 + "Position": 101.81015, + "HyperDash": false }, { "StartTime": 9062, - "Position": 134.47818 + "Position": 134.47818, + "HyperDash": false }, { "StartTime": 9125, - "Position": 141.414444 + "Position": 141.414444, + "HyperDash": false }, { "StartTime": 9187, - "Position": 164.1861 + "Position": 164.1861, + "HyperDash": false }, { "StartTime": 9250, - "Position": 176.600418 + "Position": 176.600418, + "HyperDash": false }, { "StartTime": 9312, - "Position": 184.293015 + "Position": 184.293015, + "HyperDash": false }, { "StartTime": 9375, - "Position": 212.2076 + "Position": 212.2076, + "HyperDash": false }, { "StartTime": 9437, - "Position": 236.438324 + "Position": 236.438324, + "HyperDash": false }, { "StartTime": 9500, - "Position": 237.2304 + "Position": 237.2304, + "HyperDash": false }, { "StartTime": 9562, - "Position": 241.253983 + "Position": 241.253983, + "HyperDash": false }, { "StartTime": 9625, - "Position": 233.950623 + "Position": 233.950623, + "HyperDash": false }, { "StartTime": 9687, - "Position": 265.3786 + "Position": 265.3786, + "HyperDash": false }, { "StartTime": 9750, - "Position": 236.8865 + "Position": 236.8865, + "HyperDash": false }, { "StartTime": 9812, - "Position": 273.38974 + "Position": 273.38974, + "HyperDash": false }, { "StartTime": 9875, - "Position": 267.701874 + "Position": 267.701874, + "HyperDash": false }, { "StartTime": 9937, - "Position": 263.2331 + "Position": 263.2331, + "HyperDash": false }, { "StartTime": 10000, - "Position": 270.339874 + "Position": 270.339874, + "HyperDash": false }, { "StartTime": 10062, - "Position": 291.9349 + "Position": 291.9349, + "HyperDash": false }, { "StartTime": 10125, - "Position": 294.2969 + "Position": 294.2969, + "HyperDash": false }, { "StartTime": 10187, - "Position": 307.834137 + "Position": 307.834137, + "HyperDash": false }, { "StartTime": 10250, - "Position": 310.6449 + "Position": 310.6449, + "HyperDash": false }, { "StartTime": 10312, - "Position": 344.746338 + "Position": 344.746338, + "HyperDash": false }, { "StartTime": 10375, - "Position": 349.21875 + "Position": 349.21875, + "HyperDash": false }, { "StartTime": 10437, - "Position": 373.943 + "Position": 373.943, + "HyperDash": false }, { "StartTime": 10500, - "Position": 401.0588 + "Position": 401.0588, + "HyperDash": false }, { "StartTime": 10558, - "Position": 421.21347 + "Position": 421.21347, + "HyperDash": false }, { "StartTime": 10616, - "Position": 431.6034 + "Position": 431.6034, + "HyperDash": false }, { "StartTime": 10674, - "Position": 433.835754 + "Position": 433.835754, + "HyperDash": false }, { "StartTime": 10732, - "Position": 452.5042 + "Position": 452.5042, + "HyperDash": false }, { "StartTime": 10790, - "Position": 486.290955 + "Position": 486.290955, + "HyperDash": false }, { "StartTime": 10848, - "Position": 488.943237 + "Position": 488.943237, + "HyperDash": false }, { "StartTime": 10906, - "Position": 493.3372 + "Position": 493.3372, + "HyperDash": false }, { "StartTime": 10999, - "Position": 508.166229 + "Position": 508.166229, + "HyperDash": false } ] }, @@ -563,39 +697,48 @@ "StartTime": 11500, "Objects": [{ "StartTime": 11500, - "Position": 97 + "Position": 97, + "HyperDash": false }, { "StartTime": 11562, - "Position": 267 + "Position": 267, + "HyperDash": false }, { "StartTime": 11625, - "Position": 116 + "Position": 116, + "HyperDash": false }, { "StartTime": 11687, - "Position": 451 + "Position": 451, + "HyperDash": false }, { "StartTime": 11750, - "Position": 414 + "Position": 414, + "HyperDash": false }, { "StartTime": 11812, - "Position": 88 + "Position": 88, + "HyperDash": false }, { "StartTime": 11875, - "Position": 257 + "Position": 257, + "HyperDash": false }, { "StartTime": 11937, - "Position": 175 + "Position": 175, + "HyperDash": false }, { "StartTime": 12000, - "Position": 38 + "Position": 38, + "HyperDash": false } ] }, @@ -603,263 +746,328 @@ "StartTime": 12500, "Objects": [{ "StartTime": 12500, - "Position": 512 + "Position": 512, + "HyperDash": false }, { "StartTime": 12562, - "Position": 494.3132 + "Position": 494.3132, + "HyperDash": false }, { "StartTime": 12625, - "Position": 461.3089 + "Position": 461.3089, + "HyperDash": false }, { "StartTime": 12687, - "Position": 469.6221 + "Position": 469.6221, + "HyperDash": false }, { "StartTime": 12750, - "Position": 441.617767 + "Position": 441.617767, + "HyperDash": false }, { "StartTime": 12812, - "Position": 402.930969 + "Position": 402.930969, + "HyperDash": false }, { "StartTime": 12875, - "Position": 407.926666 + "Position": 407.926666, + "HyperDash": false }, { "StartTime": 12937, - "Position": 364.239868 + "Position": 364.239868, + "HyperDash": false }, { "StartTime": 13000, - "Position": 353.235535 + "Position": 353.235535, + "HyperDash": false }, { "StartTime": 13062, - "Position": 320.548767 + "Position": 320.548767, + "HyperDash": false }, { "StartTime": 13125, - "Position": 303.544434 + "Position": 303.544434, + "HyperDash": false }, { "StartTime": 13187, - "Position": 295.857635 + "Position": 295.857635, + "HyperDash": false }, { "StartTime": 13250, - "Position": 265.853333 + "Position": 265.853333, + "HyperDash": false }, { "StartTime": 13312, - "Position": 272.166534 + "Position": 272.166534, + "HyperDash": false }, { "StartTime": 13375, - "Position": 240.1622 + "Position": 240.1622, + "HyperDash": false }, { "StartTime": 13437, - "Position": 229.4754 + "Position": 229.4754, + "HyperDash": false }, { "StartTime": 13500, - "Position": 194.471069 + "Position": 194.471069, + "HyperDash": false }, { "StartTime": 13562, - "Position": 158.784271 + "Position": 158.784271, + "HyperDash": false }, { "StartTime": 13625, - "Position": 137.779968 + "Position": 137.779968, + "HyperDash": false }, { "StartTime": 13687, - "Position": 147.09314 + "Position": 147.09314, + "HyperDash": false }, { "StartTime": 13750, - "Position": 122.088837 + "Position": 122.088837, + "HyperDash": false }, { "StartTime": 13812, - "Position": 77.40204 + "Position": 77.40204, + "HyperDash": false }, { "StartTime": 13875, - "Position": 79.3977356 + "Position": 79.3977356, + "HyperDash": false }, { "StartTime": 13937, - "Position": 56.710907 + "Position": 56.710907, + "HyperDash": false }, { "StartTime": 14000, - "Position": 35.7066345 + "Position": 35.7066345, + "HyperDash": false }, { "StartTime": 14062, - "Position": 1.01980591 + "Position": 1.01980591, + "HyperDash": false }, { "StartTime": 14125, - "Position": 0 + "Position": 0, + "HyperDash": false }, { "StartTime": 14187, - "Position": 21.7696266 + "Position": 21.7696266, + "HyperDash": false }, { "StartTime": 14250, - "Position": 49.0119171 + "Position": 49.0119171, + "HyperDash": false }, { "StartTime": 14312, - "Position": 48.9488258 + "Position": 48.9488258, + "HyperDash": false }, { "StartTime": 14375, - "Position": 87.19112 + "Position": 87.19112, + "HyperDash": false }, { "StartTime": 14437, - "Position": 97.12803 + "Position": 97.12803, + "HyperDash": false }, { "StartTime": 14500, - "Position": 118.370323 + "Position": 118.370323, + "HyperDash": false }, { "StartTime": 14562, - "Position": 130.307236 + "Position": 130.307236, + "HyperDash": false }, { "StartTime": 14625, - "Position": 154.549515 + "Position": 154.549515, + "HyperDash": false }, { "StartTime": 14687, - "Position": 190.486435 + "Position": 190.486435, + "HyperDash": false }, { "StartTime": 14750, - "Position": 211.728714 + "Position": 211.728714, + "HyperDash": false }, { "StartTime": 14812, - "Position": 197.665634 + "Position": 197.665634, + "HyperDash": false }, { "StartTime": 14875, - "Position": 214.907928 + "Position": 214.907928, + "HyperDash": false }, { "StartTime": 14937, - "Position": 263.844849 + "Position": 263.844849, + "HyperDash": false }, { "StartTime": 15000, - "Position": 271.087128 + "Position": 271.087128, + "HyperDash": false }, { "StartTime": 15062, - "Position": 270.024017 + "Position": 270.024017, + "HyperDash": false }, { "StartTime": 15125, - "Position": 308.266327 + "Position": 308.266327, + "HyperDash": false }, { "StartTime": 15187, - "Position": 313.203247 + "Position": 313.203247, + "HyperDash": false }, { "StartTime": 15250, - "Position": 328.445526 + "Position": 328.445526, + "HyperDash": false }, { "StartTime": 15312, - "Position": 370.382446 + "Position": 370.382446, + "HyperDash": false }, { "StartTime": 15375, - "Position": 387.624725 + "Position": 387.624725, + "HyperDash": false }, { "StartTime": 15437, - "Position": 421.561646 + "Position": 421.561646, + "HyperDash": false }, { "StartTime": 15500, - "Position": 423.803925 + "Position": 423.803925, + "HyperDash": false }, { "StartTime": 15562, - "Position": 444.740845 + "Position": 444.740845, + "HyperDash": false }, { "StartTime": 15625, - "Position": 469.983124 + "Position": 469.983124, + "HyperDash": false }, { "StartTime": 15687, - "Position": 473.920044 + "Position": 473.920044, + "HyperDash": false }, { "StartTime": 15750, - "Position": 501.162323 + "Position": 501.162323, + "HyperDash": false }, { "StartTime": 15812, - "Position": 488.784332 + "Position": 488.784332, + "HyperDash": false }, { "StartTime": 15875, - "Position": 466.226227 + "Position": 466.226227, + "HyperDash": false }, { "StartTime": 15937, - "Position": 445.978638 + "Position": 445.978638, + "HyperDash": false }, { "StartTime": 16000, - "Position": 446.420532 + "Position": 446.420532, + "HyperDash": false }, { "StartTime": 16058, - "Position": 428.4146 + "Position": 428.4146, + "HyperDash": false }, { "StartTime": 16116, - "Position": 420.408844 + "Position": 420.408844, + "HyperDash": false }, { "StartTime": 16174, - "Position": 374.402924 + "Position": 374.402924, + "HyperDash": false }, { "StartTime": 16232, - "Position": 371.397156 + "Position": 371.397156, + "HyperDash": false }, { "StartTime": 16290, - "Position": 350.391235 + "Position": 350.391235, + "HyperDash": false }, { "StartTime": 16348, - "Position": 340.385468 + "Position": 340.385468, + "HyperDash": false }, { "StartTime": 16406, - "Position": 337.3797 + "Position": 337.3797, + "HyperDash": false }, { "StartTime": 16500, - "Position": 291.1977 + "Position": 291.1977, + "HyperDash": false } ] }, @@ -867,71 +1075,88 @@ "StartTime": 17000, "Objects": [{ "StartTime": 17000, - "Position": 256 + "Position": 256, + "HyperDash": false }, { "StartTime": 17062, - "Position": 247.16 + "Position": 247.16, + "HyperDash": false }, { "StartTime": 17125, - "Position": 211 + "Position": 211, + "HyperDash": false }, { "StartTime": 17187, - "Position": 183.16 + "Position": 183.16, + "HyperDash": false }, { "StartTime": 17250, - "Position": 176 + "Position": 176, + "HyperDash": false }, { "StartTime": 17312, - "Position": 204.84 + "Position": 204.84, + "HyperDash": false }, { "StartTime": 17375, - "Position": 218 + "Position": 218, + "HyperDash": false }, { "StartTime": 17437, - "Position": 231.84 + "Position": 231.84, + "HyperDash": false }, { "StartTime": 17500, - "Position": 256 + "Position": 256, + "HyperDash": false }, { "StartTime": 17562, - "Position": 229.16 + "Position": 229.16, + "HyperDash": false }, { "StartTime": 17625, - "Position": 227 + "Position": 227, + "HyperDash": false }, { "StartTime": 17687, - "Position": 186.16 + "Position": 186.16, + "HyperDash": false }, { "StartTime": 17750, - "Position": 176 + "Position": 176, + "HyperDash": false }, { "StartTime": 17803, - "Position": 211.959991 + "Position": 211.959991, + "HyperDash": false }, { "StartTime": 17857, - "Position": 197.23999 + "Position": 197.23999, + "HyperDash": false }, { "StartTime": 17910, - "Position": 225.200012 + "Position": 225.200012, + "HyperDash": false }, { "StartTime": 18000, - "Position": 256 + "Position": 256, + "HyperDash": false } ] }, @@ -939,71 +1164,88 @@ "StartTime": 18500, "Objects": [{ "StartTime": 18500, - "Position": 437 + "Position": 437, + "HyperDash": false }, { "StartTime": 18559, - "Position": 289 + "Position": 289, + "HyperDash": false }, { "StartTime": 18618, - "Position": 464 + "Position": 464, + "HyperDash": false }, { "StartTime": 18678, - "Position": 36 + "Position": 36, + "HyperDash": false }, { "StartTime": 18737, - "Position": 378 + "Position": 378, + "HyperDash": false }, { "StartTime": 18796, - "Position": 297 + "Position": 297, + "HyperDash": false }, { "StartTime": 18856, - "Position": 418 + "Position": 418, + "HyperDash": false }, { "StartTime": 18915, - "Position": 329 + "Position": 329, + "HyperDash": false }, { "StartTime": 18975, - "Position": 338 + "Position": 338, + "HyperDash": false }, { "StartTime": 19034, - "Position": 394 + "Position": 394, + "HyperDash": false }, { "StartTime": 19093, - "Position": 40 + "Position": 40, + "HyperDash": false }, { "StartTime": 19153, - "Position": 13 + "Position": 13, + "HyperDash": false }, { "StartTime": 19212, - "Position": 80 + "Position": 80, + "HyperDash": false }, { "StartTime": 19271, - "Position": 138 + "Position": 138, + "HyperDash": false }, { "StartTime": 19331, - "Position": 311 + "Position": 311, + "HyperDash": false }, { "StartTime": 19390, - "Position": 216 + "Position": 216, + "HyperDash": false }, { "StartTime": 19450, - "Position": 310 + "Position": 310, + "HyperDash": false } ] }, @@ -1011,263 +1253,328 @@ "StartTime": 19875, "Objects": [{ "StartTime": 19875, - "Position": 216 + "Position": 216, + "HyperDash": false }, { "StartTime": 19937, - "Position": 228.307053 + "Position": 228.307053, + "HyperDash": false }, { "StartTime": 20000, - "Position": 214.036865 + "Position": 214.036865, + "HyperDash": false }, { "StartTime": 20062, - "Position": 224.312088 + "Position": 224.312088, + "HyperDash": false }, { "StartTime": 20125, - "Position": 253.838928 + "Position": 253.838928, + "HyperDash": false }, { "StartTime": 20187, - "Position": 259.9743 + "Position": 259.9743, + "HyperDash": false }, { "StartTime": 20250, - "Position": 299.999146 + "Position": 299.999146, + "HyperDash": false }, { "StartTime": 20312, - "Position": 289.669067 + "Position": 289.669067, + "HyperDash": false }, { "StartTime": 20375, - "Position": 317.446747 + "Position": 317.446747, + "HyperDash": false }, { "StartTime": 20437, - "Position": 344.750275 + "Position": 344.750275, + "HyperDash": false }, { "StartTime": 20500, - "Position": 328.0156 + "Position": 328.0156, + "HyperDash": false }, { "StartTime": 20562, - "Position": 331.472168 + "Position": 331.472168, + "HyperDash": false }, { "StartTime": 20625, - "Position": 302.165466 + "Position": 302.165466, + "HyperDash": false }, { "StartTime": 20687, - "Position": 303.044617 + "Position": 303.044617, + "HyperDash": false }, { "StartTime": 20750, - "Position": 306.457367 + "Position": 306.457367, + "HyperDash": false }, { "StartTime": 20812, - "Position": 265.220581 + "Position": 265.220581, + "HyperDash": false }, { "StartTime": 20875, - "Position": 270.3294 + "Position": 270.3294, + "HyperDash": false }, { "StartTime": 20937, - "Position": 257.57605 + "Position": 257.57605, + "HyperDash": false }, { "StartTime": 21000, - "Position": 247.803329 + "Position": 247.803329, + "HyperDash": false }, { "StartTime": 21062, - "Position": 225.958359 + "Position": 225.958359, + "HyperDash": false }, { "StartTime": 21125, - "Position": 201.79332 + "Position": 201.79332, + "HyperDash": false }, { "StartTime": 21187, - "Position": 170.948349 + "Position": 170.948349, + "HyperDash": false }, { "StartTime": 21250, - "Position": 146.78334 + "Position": 146.78334, + "HyperDash": false }, { "StartTime": 21312, - "Position": 149.93837 + "Position": 149.93837, + "HyperDash": false }, { "StartTime": 21375, - "Position": 119.121056 + "Position": 119.121056, + "HyperDash": false }, { "StartTime": 21437, - "Position": 133.387573 + "Position": 133.387573, + "HyperDash": false }, { "StartTime": 21500, - "Position": 117.503014 + "Position": 117.503014, + "HyperDash": false }, { "StartTime": 21562, - "Position": 103.749374 + "Position": 103.749374, + "HyperDash": false }, { "StartTime": 21625, - "Position": 127.165535 + "Position": 127.165535, + "HyperDash": false }, { "StartTime": 21687, - "Position": 113.029991 + "Position": 113.029991, + "HyperDash": false }, { "StartTime": 21750, - "Position": 101.547928 + "Position": 101.547928, + "HyperDash": false }, { "StartTime": 21812, - "Position": 133.856232 + "Position": 133.856232, + "HyperDash": false }, { "StartTime": 21875, - "Position": 124.28746 + "Position": 124.28746, + "HyperDash": false }, { "StartTime": 21937, - "Position": 121.754929 + "Position": 121.754929, + "HyperDash": false }, { "StartTime": 22000, - "Position": 155.528732 + "Position": 155.528732, + "HyperDash": false }, { "StartTime": 22062, - "Position": 142.1691 + "Position": 142.1691, + "HyperDash": false }, { "StartTime": 22125, - "Position": 186.802155 + "Position": 186.802155, + "HyperDash": false }, { "StartTime": 22187, - "Position": 198.6452 + "Position": 198.6452, + "HyperDash": false }, { "StartTime": 22250, - "Position": 191.892181 + "Position": 191.892181, + "HyperDash": false }, { "StartTime": 22312, - "Position": 232.713028 + "Position": 232.713028, + "HyperDash": false }, { "StartTime": 22375, - "Position": 240.4715 + "Position": 240.4715, + "HyperDash": false }, { "StartTime": 22437, - "Position": 278.3719 + "Position": 278.3719, + "HyperDash": false }, { "StartTime": 22500, - "Position": 288.907257 + "Position": 288.907257, + "HyperDash": false }, { "StartTime": 22562, - "Position": 297.353119 + "Position": 297.353119, + "HyperDash": false }, { "StartTime": 22625, - "Position": 301.273376 + "Position": 301.273376, + "HyperDash": false }, { "StartTime": 22687, - "Position": 339.98288 + "Position": 339.98288, + "HyperDash": false }, { "StartTime": 22750, - "Position": 353.078552 + "Position": 353.078552, + "HyperDash": false }, { "StartTime": 22812, - "Position": 363.8958 + "Position": 363.8958, + "HyperDash": false }, { "StartTime": 22875, - "Position": 398.054047 + "Position": 398.054047, + "HyperDash": false }, { "StartTime": 22937, - "Position": 419.739441 + "Position": 419.739441, + "HyperDash": false }, { "StartTime": 23000, - "Position": 435.178467 + "Position": 435.178467, + "HyperDash": false }, { "StartTime": 23062, - "Position": 420.8687 + "Position": 420.8687, + "HyperDash": false }, { "StartTime": 23125, - "Position": 448.069977 + "Position": 448.069977, + "HyperDash": false }, { "StartTime": 23187, - "Position": 425.688477 + "Position": 425.688477, + "HyperDash": false }, { "StartTime": 23250, - "Position": 426.9612 + "Position": 426.9612, + "HyperDash": false }, { "StartTime": 23312, - "Position": 454.92807 + "Position": 454.92807, + "HyperDash": false }, { "StartTime": 23375, - "Position": 439.749878 + "Position": 439.749878, + "HyperDash": false }, { "StartTime": 23433, - "Position": 440.644684 + "Position": 440.644684, + "HyperDash": false }, { "StartTime": 23491, - "Position": 445.7359 + "Position": 445.7359, + "HyperDash": false }, { "StartTime": 23549, - "Position": 432.0944 + "Position": 432.0944, + "HyperDash": false }, { "StartTime": 23607, - "Position": 415.796173 + "Position": 415.796173, + "HyperDash": false }, { "StartTime": 23665, - "Position": 407.897461 + "Position": 407.897461, + "HyperDash": false }, { "StartTime": 23723, - "Position": 409.462555 + "Position": 409.462555, + "HyperDash": false }, { "StartTime": 23781, - "Position": 406.53775 + "Position": 406.53775, + "HyperDash": false }, { "StartTime": 23874, - "Position": 408.720825 + "Position": 408.720825, + "HyperDash": false } ] } diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider-expected-conversion.json index 83f9e30800..081b574c5b 100644 --- a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider-expected-conversion.json +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider-expected-conversion.json @@ -3,147 +3,183 @@ "StartTime": 369, "Objects": [{ "StartTime": 369, - "Position": 177 + "Position": 177, + "HyperDash": false }, { "StartTime": 450, - "Position": 216.539276 + "Position": 216.539276, + "HyperDash": false }, { "StartTime": 532, - "Position": 256.5667 + "Position": 256.5667, + "HyperDash": false }, { "StartTime": 614, - "Position": 296.594116 + "Position": 296.594116, + "HyperDash": false }, { "StartTime": 696, - "Position": 336.621521 + "Position": 336.621521, + "HyperDash": false }, { "StartTime": 778, - "Position": 376.99762 + "Position": 376.99762, + "HyperDash": false }, { "StartTime": 860, - "Position": 337.318878 + "Position": 337.318878, + "HyperDash": false }, { "StartTime": 942, - "Position": 297.291443 + "Position": 297.291443, + "HyperDash": false }, { "StartTime": 1024, - "Position": 257.264038 + "Position": 257.264038, + "HyperDash": false }, { "StartTime": 1106, - "Position": 217.2366 + "Position": 217.2366, + "HyperDash": false }, { "StartTime": 1188, - "Position": 177 + "Position": 177, + "HyperDash": false }, { "StartTime": 1270, - "Position": 216.818192 + "Position": 216.818192, + "HyperDash": false }, { "StartTime": 1352, - "Position": 256.8456 + "Position": 256.8456, + "HyperDash": false }, { "StartTime": 1434, - "Position": 296.873047 + "Position": 296.873047, + "HyperDash": false }, { "StartTime": 1516, - "Position": 336.900452 + "Position": 336.900452, + "HyperDash": false }, { "StartTime": 1598, - "Position": 376.99762 + "Position": 376.99762, + "HyperDash": false }, { "StartTime": 1680, - "Position": 337.039948 + "Position": 337.039948, + "HyperDash": false }, { "StartTime": 1762, - "Position": 297.0125 + "Position": 297.0125, + "HyperDash": false }, { "StartTime": 1844, - "Position": 256.9851 + "Position": 256.9851, + "HyperDash": false }, { "StartTime": 1926, - "Position": 216.957672 + "Position": 216.957672, + "HyperDash": false }, { "StartTime": 2008, - "Position": 177 + "Position": 177, + "HyperDash": false }, { "StartTime": 2090, - "Position": 217.097137 + "Position": 217.097137, + "HyperDash": false }, { "StartTime": 2172, - "Position": 257.124573 + "Position": 257.124573, + "HyperDash": false }, { "StartTime": 2254, - "Position": 297.152 + "Position": 297.152, + "HyperDash": false }, { "StartTime": 2336, - "Position": 337.179443 + "Position": 337.179443, + "HyperDash": false }, { "StartTime": 2418, - "Position": 376.99762 + "Position": 376.99762, + "HyperDash": false }, { "StartTime": 2500, - "Position": 336.760956 + "Position": 336.760956, + "HyperDash": false }, { "StartTime": 2582, - "Position": 296.733643 + "Position": 296.733643, + "HyperDash": false }, { "StartTime": 2664, - "Position": 256.7062 + "Position": 256.7062, + "HyperDash": false }, { "StartTime": 2746, - "Position": 216.678772 + "Position": 216.678772, + "HyperDash": false }, { "StartTime": 2828, - "Position": 177 + "Position": 177, + "HyperDash": false }, { "StartTime": 2909, - "Position": 216.887909 + "Position": 216.887909, + "HyperDash": false }, { "StartTime": 2991, - "Position": 256.915344 + "Position": 256.915344, + "HyperDash": false }, { "StartTime": 3073, - "Position": 296.942749 + "Position": 296.942749, + "HyperDash": false }, { "StartTime": 3155, - "Position": 336.970184 + "Position": 336.970184, + "HyperDash": false }, { "StartTime": 3237, - "Position": 376.99762 + "Position": 376.99762, + "HyperDash": false } ] }] diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner-expected-conversion.json index 7333b600fb..01f474c149 100644 --- a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner-expected-conversion.json +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner-expected-conversion.json @@ -3,71 +3,88 @@ "StartTime": 369, "Objects": [{ "StartTime": 369, - "Position": 65 + "Position": 65, + "HyperDash": false }, { "StartTime": 450, - "Position": 482 + "Position": 482, + "HyperDash": false }, { "StartTime": 532, - "Position": 164 + "Position": 164, + "HyperDash": false }, { "StartTime": 614, - "Position": 315 + "Position": 315, + "HyperDash": false }, { "StartTime": 696, - "Position": 145 + "Position": 145, + "HyperDash": false }, { "StartTime": 778, - "Position": 159 + "Position": 159, + "HyperDash": false }, { "StartTime": 860, - "Position": 310 + "Position": 310, + "HyperDash": false }, { "StartTime": 942, - "Position": 441 + "Position": 441, + "HyperDash": false }, { "StartTime": 1024, - "Position": 428 + "Position": 428, + "HyperDash": false }, { "StartTime": 1106, - "Position": 243 + "Position": 243, + "HyperDash": false }, { "StartTime": 1188, - "Position": 422 + "Position": 422, + "HyperDash": false }, { "StartTime": 1270, - "Position": 481 + "Position": 481, + "HyperDash": false }, { "StartTime": 1352, - "Position": 104 + "Position": 104, + "HyperDash": false }, { "StartTime": 1434, - "Position": 473 + "Position": 473, + "HyperDash": false }, { "StartTime": 1516, - "Position": 135 + "Position": 135, + "HyperDash": false }, { "StartTime": 1598, - "Position": 360 + "Position": 360, + "HyperDash": false }, { "StartTime": 1680, - "Position": 123 + "Position": 123, + "HyperDash": false } ] }] diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream-expected-conversion.json index bbc16ab912..8eaaf3bb90 100644 --- a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream-expected-conversion.json +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream-expected-conversion.json @@ -3,231 +3,264 @@ "StartTime": 369, "Objects": [{ "StartTime": 369, - "Position": 258 + "Position": 258, + "HyperDash": false }] }, { "StartTime": 450, "Objects": [{ "StartTime": 450, - "Position": 254 + "Position": 254, + "HyperDash": false }] }, { "StartTime": 532, "Objects": [{ "StartTime": 532, - "Position": 241 + "Position": 241, + "HyperDash": false }] }, { "StartTime": 614, "Objects": [{ "StartTime": 614, - "Position": 238 + "Position": 238, + "HyperDash": false }] }, { "StartTime": 696, "Objects": [{ "StartTime": 696, - "Position": 238 + "Position": 238, + "HyperDash": false }] }, { "StartTime": 778, "Objects": [{ "StartTime": 778, - "Position": 278 + "Position": 278, + "HyperDash": false }] }, { "StartTime": 860, "Objects": [{ "StartTime": 860, - "Position": 238 + "Position": 238, + "HyperDash": false }] }, { "StartTime": 942, "Objects": [{ "StartTime": 942, - "Position": 278 + "Position": 278, + "HyperDash": false }] }, { "StartTime": 1024, "Objects": [{ "StartTime": 1024, - "Position": 238 + "Position": 238, + "HyperDash": false }] }, { "StartTime": 1106, "Objects": [{ "StartTime": 1106, - "Position": 278 + "Position": 278, + "HyperDash": false }] }, { "StartTime": 1188, "Objects": [{ "StartTime": 1188, - "Position": 278 + "Position": 278, + "HyperDash": false }] }, { "StartTime": 1270, "Objects": [{ "StartTime": 1270, - "Position": 278 + "Position": 278, + "HyperDash": false }] }, { "StartTime": 1352, "Objects": [{ "StartTime": 1352, - "Position": 238 + "Position": 238, + "HyperDash": false }] }, { "StartTime": 1434, "Objects": [{ "StartTime": 1434, - "Position": 258 + "Position": 258, + "HyperDash": false }] }, { "StartTime": 1516, "Objects": [{ "StartTime": 1516, - "Position": 253 + "Position": 253, + "HyperDash": false }] }, { "StartTime": 1598, "Objects": [{ "StartTime": 1598, - "Position": 238 + "Position": 238, + "HyperDash": false }] }, { "StartTime": 1680, "Objects": [{ "StartTime": 1680, - "Position": 260 + "Position": 260, + "HyperDash": false }] }, { "StartTime": 1762, "Objects": [{ "StartTime": 1762, - "Position": 238 + "Position": 238, + "HyperDash": false }] }, { "StartTime": 1844, "Objects": [{ "StartTime": 1844, - "Position": 278 + "Position": 278, + "HyperDash": false }] }, { "StartTime": 1926, "Objects": [{ "StartTime": 1926, - "Position": 278 + "Position": 278, + "HyperDash": false }] }, { "StartTime": 2008, "Objects": [{ "StartTime": 2008, - "Position": 238 + "Position": 238, + "HyperDash": false }] }, { "StartTime": 2090, "Objects": [{ "StartTime": 2090, - "Position": 238 + "Position": 238, + "HyperDash": false }] }, { "StartTime": 2172, "Objects": [{ "StartTime": 2172, - "Position": 243 + "Position": 243, + "HyperDash": false }] }, { "StartTime": 2254, "Objects": [{ "StartTime": 2254, - "Position": 278 + "Position": 278, + "HyperDash": false }] }, { "StartTime": 2336, "Objects": [{ "StartTime": 2336, - "Position": 278 + "Position": 278, + "HyperDash": false }] }, { "StartTime": 2418, "Objects": [{ "StartTime": 2418, - "Position": 238 + "Position": 238, + "HyperDash": false }] }, { "StartTime": 2500, "Objects": [{ "StartTime": 2500, - "Position": 258 + "Position": 258, + "HyperDash": false }] }, { "StartTime": 2582, "Objects": [{ "StartTime": 2582, - "Position": 256 + "Position": 256, + "HyperDash": false }] }, { "StartTime": 2664, "Objects": [{ "StartTime": 2664, - "Position": 242 + "Position": 242, + "HyperDash": false }] }, { "StartTime": 2746, "Objects": [{ "StartTime": 2746, - "Position": 238 + "Position": 238, + "HyperDash": false }] }, { "StartTime": 2828, "Objects": [{ "StartTime": 2828, - "Position": 238 + "Position": 238, + "HyperDash": false }] }, { "StartTime": 2909, "Objects": [{ "StartTime": 2909, - "Position": 271 + "Position": 271, + "HyperDash": false }] }, { "StartTime": 2991, "Objects": [{ "StartTime": 2991, - "Position": 254 + "Position": 254, + "HyperDash": false }] } ] diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json index 3bde97070c..5060389ad8 100644 --- a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json @@ -3,14 +3,16 @@ "StartTime": 3368, "Objects": [{ "StartTime": 3368, - "Position": 374 + "Position": 374, + "HyperDash": false }] }, { "StartTime": 3501, "Objects": [{ "StartTime": 3501, - "Position": 446 + "Position": 446, + "HyperDash": false }] } ] diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/slider-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/slider-expected-conversion.json index 58c52b6867..2378ba5511 100644 --- a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/slider-expected-conversion.json +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/slider-expected-conversion.json @@ -1 +1,71 @@ -{"Mappings":[{"StartTime":19184.0,"Objects":[{"StartTime":19184.0,"Position":320.0},{"StartTime":19263.0,"Position":311.730255},{"StartTime":19343.0,"Position":324.6205},{"StartTime":19423.0,"Position":343.0907},{"StartTime":19503.0,"Position":372.2917},{"StartTime":19582.0,"Position":385.194733},{"StartTime":19662.0,"Position":379.0426},{"StartTime":19742.0,"Position":385.1066},{"StartTime":19822.0,"Position":391.624664},{"StartTime":19919.0,"Position":386.27832},{"StartTime":20016.0,"Position":380.117035},{"StartTime":20113.0,"Position":381.664154},{"StartTime":20247.0,"Position":370.872864}]}]} \ No newline at end of file +{ + "Mappings": [{ + "StartTime": 19184, + "Objects": [{ + "StartTime": 19184, + "Position": 320, + "HyperDash": false + }, + { + "StartTime": 19263, + "Position": 311.730255, + "HyperDash": false + }, + { + "StartTime": 19343, + "Position": 324.6205, + "HyperDash": false + }, + { + "StartTime": 19423, + "Position": 343.0907, + "HyperDash": false + }, + { + "StartTime": 19503, + "Position": 372.2917, + "HyperDash": false + }, + { + "StartTime": 19582, + "Position": 385.194733, + "HyperDash": false + }, + { + "StartTime": 19662, + "Position": 379.0426, + "HyperDash": false + }, + { + "StartTime": 19742, + "Position": 385.1066, + "HyperDash": false + }, + { + "StartTime": 19822, + "Position": 391.624664, + "HyperDash": false + }, + { + "StartTime": 19919, + "Position": 386.27832, + "HyperDash": false + }, + { + "StartTime": 20016, + "Position": 380.117035, + "HyperDash": false + }, + { + "StartTime": 20113, + "Position": 381.664154, + "HyperDash": false + }, + { + "StartTime": 20247, + "Position": 370.872864, + "HyperDash": false + } + ] + }] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-and-circles-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-and-circles-expected-conversion.json index dd81947f1c..abd5b2afd1 100644 --- a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-and-circles-expected-conversion.json +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-and-circles-expected-conversion.json @@ -3,18 +3,21 @@ "StartTime": 2589, "Objects": [{ "StartTime": 2589, - "Position": 256 + "Position": 256, + "HyperDash": false }] }, { "StartTime": 2915, "Objects": [{ "StartTime": 2915, - "Position": 65 + "Position": 65, + "HyperDash": false }, { "StartTime": 2916, - "Position": 482 + "Position": 482, + "HyperDash": false } ] }, @@ -22,11 +25,13 @@ "StartTime": 3078, "Objects": [{ "StartTime": 3078, - "Position": 164 + "Position": 164, + "HyperDash": false }, { "StartTime": 3079, - "Position": 315 + "Position": 315, + "HyperDash": false } ] }, @@ -34,11 +39,13 @@ "StartTime": 3241, "Objects": [{ "StartTime": 3241, - "Position": 145 + "Position": 145, + "HyperDash": false }, { "StartTime": 3242, - "Position": 159 + "Position": 159, + "HyperDash": false } ] }, @@ -46,11 +53,13 @@ "StartTime": 3404, "Objects": [{ "StartTime": 3404, - "Position": 310 + "Position": 310, + "HyperDash": false }, { "StartTime": 3405, - "Position": 441 + "Position": 441, + "HyperDash": false } ] }, @@ -58,7 +67,8 @@ "StartTime": 5197, "Objects": [{ "StartTime": 5197, - "Position": 256 + "Position": 256, + "HyperDash": false }] } ] diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-expected-conversion.json index b69b1ae056..8a7847e065 100644 --- a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-expected-conversion.json +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-expected-conversion.json @@ -3,71 +3,88 @@ "StartTime": 18500, "Objects": [{ "StartTime": 18500, - "Position": 65 + "Position": 65, + "HyperDash": false }, { "StartTime": 18559, - "Position": 482 + "Position": 482, + "HyperDash": false }, { "StartTime": 18618, - "Position": 164 + "Position": 164, + "HyperDash": false }, { "StartTime": 18678, - "Position": 315 + "Position": 315, + "HyperDash": false }, { "StartTime": 18737, - "Position": 145 + "Position": 145, + "HyperDash": false }, { "StartTime": 18796, - "Position": 159 + "Position": 159, + "HyperDash": false }, { "StartTime": 18856, - "Position": 310 + "Position": 310, + "HyperDash": false }, { "StartTime": 18915, - "Position": 441 + "Position": 441, + "HyperDash": false }, { "StartTime": 18975, - "Position": 428 + "Position": 428, + "HyperDash": false }, { "StartTime": 19034, - "Position": 243 + "Position": 243, + "HyperDash": false }, { "StartTime": 19093, - "Position": 422 + "Position": 422, + "HyperDash": false }, { "StartTime": 19153, - "Position": 481 + "Position": 481, + "HyperDash": false }, { "StartTime": 19212, - "Position": 104 + "Position": 104, + "HyperDash": false }, { "StartTime": 19271, - "Position": 473 + "Position": 473, + "HyperDash": false }, { "StartTime": 19331, - "Position": 135 + "Position": 135, + "HyperDash": false }, { "StartTime": 19390, - "Position": 360 + "Position": 360, + "HyperDash": false }, { "StartTime": 19450, - "Position": 123 + "Position": 123, + "HyperDash": false } ] }] From 8a81392d2babc053a12a96139282cad63cf9ffbe Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Dec 2021 19:26:36 +0900 Subject: [PATCH 374/419] Fix use of incorrect variable, add test --- .../CatchBeatmapConversionTest.cs | 3 ++- .../basic-hyperdash-expected-conversion.json | 19 +++++++++++++++++ .../Testing/Beatmaps/basic-hyperdash.osu | 21 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-hyperdash-expected-conversion.json create mode 100644 osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-hyperdash.osu diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs index 3352982c20..be1885cfa6 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs @@ -27,6 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests [TestCase("hardrock-repeat-slider", new[] { typeof(CatchModHardRock) })] [TestCase("hardrock-spinner", new[] { typeof(CatchModHardRock) })] [TestCase("right-bound-hr-offset", new[] { typeof(CatchModHardRock) })] + [TestCase("basic-hyperdash")] public new void Test(string name, params Type[] mods) => base.Test(name, mods); protected override IEnumerable CreateConvertValue(HitObject hitObject) @@ -100,6 +101,6 @@ namespace osu.Game.Rulesets.Catch.Tests public bool Equals(ConvertValue other) => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience) && Precision.AlmostEquals(Position, other.Position, conversion_lenience) - && HyperDash == other.hyperDash; + && HyperDash == other.HyperDash; } } diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-hyperdash-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-hyperdash-expected-conversion.json new file mode 100644 index 0000000000..b2e9431f13 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-hyperdash-expected-conversion.json @@ -0,0 +1,19 @@ +{ + "Mappings": [{ + "StartTime": 369, + "Objects": [{ + "StartTime": 369, + "Position": 0, + "HyperDash": true + }] + }, + { + "StartTime": 450, + "Objects": [{ + "StartTime": 450, + "Position": 512, + "HyperDash": false + }] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-hyperdash.osu b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-hyperdash.osu new file mode 100644 index 0000000000..db07f8c30e --- /dev/null +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-hyperdash.osu @@ -0,0 +1,21 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 +Mode: 2 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:9.6 +ApproachRate:9.6 +SliderMultiplier:1.9 +SliderTickRate:1 + +[TimingPoints] +2169,266.666666666667,4,2,1,70,1,0 + + +[HitObjects] +0,192,369,1,0,0:0:0:0: +512,192,450,1,0,0:0:0:0: From c08b6cf16050b73abfa8b290aec93ad95503f863 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Dec 2021 19:53:22 +0900 Subject: [PATCH 375/419] Remove unnecessary `StartAsync` call on `TcpIpcProvider` --- osu.Desktop/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index a9e3575a49..7ec7d53a7e 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -90,7 +90,6 @@ namespace osu.Desktop Logger.Log("Starting legacy IPC provider..."); legacyIpc = new LegacyTcpIpcProvider(); legacyIpc.Bind(); - legacyIpc.StartAsync(); } catch (Exception ex) { From abb617a3df76c0ed685f9b84ae4abd4b3ea8be79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Dec 2021 19:57:24 +0900 Subject: [PATCH 376/419] Avoid blocking `Active` state propagation --- osu.Game.Rulesets.Osu/OsuInputManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs index f5fc3de381..5c6b907e42 100644 --- a/osu.Game.Rulesets.Osu/OsuInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; +using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges.Events; @@ -44,8 +45,10 @@ namespace osu.Game.Rulesets.Osu { if (!AllowUserCursorMovement) { - // Still allow for forwarding of the "touch" part, but block the positional data. - e = new TouchStateChangeEvent(e.State, e.Input, e.Touch, false, null); + // Still allow for forwarding of the "touch" part, but replace the positional data with that of the mouse. + // Primarily relied upon by the "autopilot" osu! mod. + var touch = new Touch(e.Touch.Source, CurrentState.Mouse.Position); + e = new TouchStateChangeEvent(e.State, e.Input, touch, e.IsActive, null); } return base.HandleMouseTouchStateChange(e); From eecb1ce9f55084d19eea03378b9cf6c34cfe9706 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Dec 2021 20:20:02 +0900 Subject: [PATCH 377/419] Avoid applying mouse down effects to menu cursor when it isn't visible Closes #16114. --- osu.Game/Graphics/Cursor/MenuCursor.cs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index 3fa90e2330..8e272f637f 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -72,18 +72,21 @@ namespace osu.Game.Graphics.Cursor protected override bool OnMouseDown(MouseDownEvent e) { - // only trigger animation for main mouse buttons - activeCursor.Scale = new Vector2(1); - activeCursor.ScaleTo(0.90f, 800, Easing.OutQuint); - - activeCursor.AdditiveLayer.Alpha = 0; - activeCursor.AdditiveLayer.FadeInFromZero(800, Easing.OutQuint); - - if (cursorRotate.Value && dragRotationState != DragRotationState.Rotating) + if (State.Value == Visibility.Visible) { - // if cursor is already rotating don't reset its rotate origin - dragRotationState = DragRotationState.DragStarted; - positionMouseDown = e.MousePosition; + // only trigger animation for main mouse buttons + activeCursor.Scale = new Vector2(1); + activeCursor.ScaleTo(0.90f, 800, Easing.OutQuint); + + activeCursor.AdditiveLayer.Alpha = 0; + activeCursor.AdditiveLayer.FadeInFromZero(800, Easing.OutQuint); + + if (cursorRotate.Value && dragRotationState != DragRotationState.Rotating) + { + // if cursor is already rotating don't reset its rotate origin + dragRotationState = DragRotationState.DragStarted; + positionMouseDown = e.MousePosition; + } } return base.OnMouseDown(e); From 6bfe973fe5a17a75186ff8e2dad30f3d2d5dac26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Dec 2021 13:44:52 +0900 Subject: [PATCH 378/419] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 1131203a95..563836d29b 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index feae990df7..0e8486cabc 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 27ac1bf647..42d8962c14 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From 81603a064529d8356a0c3fff86ad603ae7c6e032 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Dec 2021 19:32:58 +0900 Subject: [PATCH 379/419] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 563836d29b..209b8cd63e 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 0e8486cabc..97bdf00f69 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 42d8962c14..d4b7339900 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From 883fcf26044bef614e4d6d08d9e4719acb537ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Dec 2021 11:44:36 +0100 Subject: [PATCH 380/419] Fix tests --- .../Visual/Beatmaps/TestSceneBeatmapCard.cs | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs index 55dbf89334..2b98c61e9b 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs @@ -255,6 +255,12 @@ namespace osu.Game.Tests.Visual.Beatmaps createTestCase(beatmapSetInfo => new BeatmapCard(beatmapSetInfo)); } + [Test] + public void TestExtra() + { + createTestCase(beatmapSetInfo => new BeatmapCardExtra(beatmapSetInfo)); + } + [Test] public void TestHoverState() { @@ -280,19 +286,5 @@ namespace osu.Game.Tests.Visual.Beatmaps BeatmapCard firstCard() => this.ChildrenOfType().First(); } - - [Test] - public void TestExtra() - { - createTestCase(beatmapSetInfo => new BeatmapCardExtra(beatmapSetInfo)); - - AddToggleStep("toggle expanded state", expanded => - { - var card = this.ChildrenOfType().Last(); - if (!card.Expanded.Disabled) - card.Expanded.Value = expanded; - }); - AddToggleStep("disable/enable expansion", disabled => this.ChildrenOfType().ForEach(card => card.Expanded.Disabled = disabled)); - } } } From 3fa45479b0cc3c5d0dbd9e024f22cf5b954d29a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Dec 2021 11:13:07 +0100 Subject: [PATCH 381/419] Share hype/nomination statistic show logic --- osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs | 12 ++++++------ .../Beatmaps/Drawables/Cards/BeatmapCardExtra.cs | 12 ++++++------ .../Drawables/Cards/Statistics/HypesStatistic.cs | 7 ++++++- .../Cards/Statistics/NominationsStatistic.cs | 9 ++++++++- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 1e24501426..14c29f8e99 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -380,13 +380,13 @@ namespace osu.Game.Beatmaps.Drawables.Cards private IEnumerable createStatistics() { - if (beatmapSet.HypeStatus != null) - yield return new HypesStatistic(beatmapSet.HypeStatus); + var hypesStatistic = HypesStatistic.CreateFor(beatmapSet); + if (hypesStatistic != null) + yield return hypesStatistic; - // web does not show nominations unless hypes are also present. - // see: https://github.com/ppy/osu-web/blob/8ed7d071fd1d3eaa7e43cf0e4ff55ca2fef9c07c/resources/assets/lib/beatmapset-panel.tsx#L443 - if (beatmapSet.HypeStatus != null && beatmapSet.NominationStatus != null) - yield return new NominationsStatistic(beatmapSet.NominationStatus); + var nominationsStatistic = NominationsStatistic.CreateFor(beatmapSet); + if (nominationsStatistic != null) + yield return nominationsStatistic; yield return new FavouritesStatistic(beatmapSet) { Current = favouriteState }; yield return new PlayCountStatistic(beatmapSet); diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index 8d3c606d76..2ccc732119 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -409,13 +409,13 @@ namespace osu.Game.Beatmaps.Drawables.Cards statisticsContainer.Content[1][0] = withMargin(new PlayCountStatistic(beatmapSet)); - if (beatmapSet.HypeStatus != null) - statisticsContainer.Content[0][1] = withMargin(new HypesStatistic(beatmapSet.HypeStatus)); + var hypesStatistic = HypesStatistic.CreateFor(beatmapSet); + if (hypesStatistic != null) + statisticsContainer.Content[0][1] = withMargin(hypesStatistic); - // web does not show nominations unless hypes are also present. - // see: https://github.com/ppy/osu-web/blob/8ed7d071fd1d3eaa7e43cf0e4ff55ca2fef9c07c/resources/assets/lib/beatmapset-panel.tsx#L443 - if (beatmapSet.HypeStatus != null && beatmapSet.NominationStatus != null) - statisticsContainer.Content[1][1] = withMargin(new NominationsStatistic(beatmapSet.NominationStatus)); + var nominationsStatistic = NominationsStatistic.CreateFor(beatmapSet); + if (nominationsStatistic != null) + statisticsContainer.Content[1][1] = withMargin(nominationsStatistic); var dateStatistic = BeatmapCardDateStatistic.CreateFor(beatmapSet); if (dateStatistic != null) diff --git a/osu.Game/Beatmaps/Drawables/Cards/Statistics/HypesStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/HypesStatistic.cs index 3fe31c7a41..521d1a5f21 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Statistics/HypesStatistic.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/HypesStatistic.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics.Sprites; using osu.Game.Resources.Localisation.Web; @@ -12,11 +14,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics /// public class HypesStatistic : BeatmapCardStatistic { - public HypesStatistic(BeatmapSetHypeStatus hypeStatus) + private HypesStatistic(BeatmapSetHypeStatus hypeStatus) { Icon = FontAwesome.Solid.Bullhorn; Text = hypeStatus.Current.ToLocalisableString(); TooltipText = BeatmapsStrings.HypeRequiredText(hypeStatus.Current.ToLocalisableString(), hypeStatus.Required.ToLocalisableString()); } + + public static HypesStatistic? CreateFor(IBeatmapSetOnlineInfo beatmapSetOnlineInfo) + => beatmapSetOnlineInfo.HypeStatus == null ? null : new HypesStatistic(beatmapSetOnlineInfo.HypeStatus); } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/Statistics/NominationsStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/NominationsStatistic.cs index f09269a615..23bd6ef0a9 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Statistics/NominationsStatistic.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/NominationsStatistic.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics.Sprites; using osu.Game.Resources.Localisation.Web; @@ -12,11 +14,16 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics /// public class NominationsStatistic : BeatmapCardStatistic { - public NominationsStatistic(BeatmapSetNominationStatus nominationStatus) + private NominationsStatistic(BeatmapSetNominationStatus nominationStatus) { Icon = FontAwesome.Solid.ThumbsUp; Text = nominationStatus.Current.ToLocalisableString(); TooltipText = BeatmapsStrings.NominationsRequiredText(nominationStatus.Current.ToLocalisableString(), nominationStatus.Required.ToLocalisableString()); } + + public static NominationsStatistic? CreateFor(IBeatmapSetOnlineInfo beatmapSetOnlineInfo) + // web does not show nominations unless hypes are also present. + // see: https://github.com/ppy/osu-web/blob/8ed7d071fd1d3eaa7e43cf0e4ff55ca2fef9c07c/resources/assets/lib/beatmapset-panel.tsx#L443 + => beatmapSetOnlineInfo.HypeStatus == null || beatmapSetOnlineInfo.NominationStatus == null ? null : new NominationsStatistic(beatmapSetOnlineInfo.NominationStatus); } } From 7aab12d4b061d6621e5c3b5e5bc36be8a75e8c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Dec 2021 11:27:38 +0100 Subject: [PATCH 382/419] Share extra row dropdown show/cancel show logic --- .../Beatmaps/Drawables/Cards/BeatmapCard.cs | 38 +++++++------------ .../Drawables/Cards/BeatmapCardExtra.cs | 38 +++++++------------ .../Cards/BeatmapCardExtraInfoRow.cs | 25 +++++++++++- 3 files changed, 49 insertions(+), 52 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 14c29f8e99..4892abc846 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -44,7 +44,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards private readonly BeatmapDownloadTracker downloadTracker; - private BeatmapCardContent content = null!; + [Cached] + private readonly BeatmapCardContent content; private BeatmapCardThumbnail thumbnail = null!; @@ -72,6 +73,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards this.beatmapSet = beatmapSet; favouriteState = new Bindable(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount)); downloadTracker = new BeatmapDownloadTracker(beatmapSet); + content = new BeatmapCardContent(height); } [BackgroundDependencyLoader(true)] @@ -80,13 +82,13 @@ namespace osu.Game.Beatmaps.Drawables.Cards Width = width; Height = height; - FillFlowContainer leftIconArea; - GridContainer titleContainer; - GridContainer artistContainer; + FillFlowContainer leftIconArea = null!; + GridContainer titleContainer = null!; + GridContainer artistContainer = null!; - InternalChild = content = new BeatmapCardContent(height) + InternalChild = content.With(c => { - MainContent = new Container + c.MainContent = new Container { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -281,20 +283,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards ChildrenEnumerable = createStatistics() }, new BeatmapCardExtraInfoRow(beatmapSet) - { - Hovered = _ => - { - content.ExpandAfterDelay(); - return false; - }, - Unhovered = _ => - { - // Handles the case where a user has not shown explicit intent to view expanded info. - // ie. quickly moved over the info row area but didn't remain within it. - if (!Expanded.Value) - content.CancelExpand(); - } - } } }, downloadProgressBar = new BeatmapCardDownloadProgressBar @@ -311,16 +299,16 @@ namespace osu.Game.Beatmaps.Drawables.Cards } } } - }, - ExpandedContent = new Container + }; + c.ExpandedContent = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Horizontal = 10, Vertical = 13 }, Child = new BeatmapCardDifficultyList(beatmapSet) - }, - Expanded = { BindTarget = Expanded } - }; + }; + c.Expanded.BindTarget = Expanded; + }); if (beatmapSet.HasVideo) leftIconArea.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) }); diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index 2ccc732119..328294a323 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -40,7 +40,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards private readonly BeatmapDownloadTracker downloadTracker; - private BeatmapCardContent content = null!; + [Cached] + private readonly BeatmapCardContent content; private BeatmapCardThumbnail thumbnail = null!; @@ -66,6 +67,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards this.beatmapSet = beatmapSet; favouriteState = new Bindable(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount)); downloadTracker = new BeatmapDownloadTracker(beatmapSet); + content = new BeatmapCardContent(height); } [BackgroundDependencyLoader(true)] @@ -74,13 +76,13 @@ namespace osu.Game.Beatmaps.Drawables.Cards Width = width; Height = height; - FillFlowContainer leftIconArea; - GridContainer titleContainer; - GridContainer artistContainer; + FillFlowContainer leftIconArea = null!; + GridContainer titleContainer = null!; + GridContainer artistContainer = null!; - InternalChild = content = new BeatmapCardContent(height) + InternalChild = content.With(c => { - MainContent = new Container + c.MainContent = new Container { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -295,20 +297,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards } }, new BeatmapCardExtraInfoRow(beatmapSet) - { - Hovered = _ => - { - content.ExpandAfterDelay(); - return false; - }, - Unhovered = _ => - { - // This hide should only trigger if the expanded content has not shown yet. - // ie. if the user has not shown intent to want to see it (quickly moved over the info row area). - if (!Expanded.Value) - content.CancelExpand(); - } - } } }, downloadProgressBar = new BeatmapCardDownloadProgressBar @@ -325,16 +313,16 @@ namespace osu.Game.Beatmaps.Drawables.Cards } } } - }, - ExpandedContent = new Container + }; + c.ExpandedContent = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Horizontal = 10, Vertical = 13 }, Child = new BeatmapCardDifficultyList(beatmapSet) - }, - Expanded = { BindTarget = Expanded } - }; + }; + c.Expanded.BindTarget = Expanded; + }); if (beatmapSet.HasVideo) leftIconArea.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) }); diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs index 0a9d98e621..2d411ad344 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs @@ -1,21 +1,28 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; using osu.Game.Online.API.Requests.Responses; using osuTK; namespace osu.Game.Beatmaps.Drawables.Cards { - public class BeatmapCardExtraInfoRow : HoverHandlingContainer + public class BeatmapCardExtraInfoRow : CompositeDrawable { + [Resolved(CanBeNull = true)] + private BeatmapCardContent? content { get; set; } + public BeatmapCardExtraInfoRow(APIBeatmapSet beatmapSet) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Child = new FillFlowContainer + InternalChild = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -39,5 +46,19 @@ namespace osu.Game.Beatmaps.Drawables.Cards } }; } + + protected override bool OnHover(HoverEvent e) + { + content?.ExpandAfterDelay(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + if (content?.Expanded.Value == false) + content.CancelExpand(); + + base.OnHoverLost(e); + } } } From f052b47d873cbe41277295a6ac75525a72fe996e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Dec 2021 12:58:05 +0100 Subject: [PATCH 383/419] Extract collapsible button container for card usage --- .../Beatmaps/Drawables/Cards/BeatmapCard.cs | 104 +--------- .../Drawables/Cards/BeatmapCardExtra.cs | 104 +--------- .../Cards/CollapsibleButtonContainer.cs | 184 ++++++++++++++++++ 3 files changed, 200 insertions(+), 192 deletions(-) create mode 100644 osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 4892abc846..13c4cfe207 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -8,11 +8,9 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; -using osu.Game.Beatmaps.Drawables.Cards.Buttons; using osu.Game.Beatmaps.Drawables.Cards.Statistics; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -24,7 +22,6 @@ using osu.Game.Overlays; using osu.Game.Overlays.BeatmapSet; using osuTK; using osu.Game.Resources.Localisation.Web; -using DownloadButton = osu.Game.Beatmaps.Drawables.Cards.Buttons.DownloadButton; namespace osu.Game.Beatmaps.Drawables.Cards { @@ -37,7 +34,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards private const float width = 408; private const float height = 100; - private const float icon_area_width = 30; private readonly APIBeatmapSet beatmapSet; private readonly Bindable favouriteState; @@ -48,20 +44,13 @@ namespace osu.Game.Beatmaps.Drawables.Cards private readonly BeatmapCardContent content; private BeatmapCardThumbnail thumbnail = null!; + private CollapsibleButtonContainer buttonContainer = null!; - private Container rightAreaBackground = null!; - private Container rightAreaButtons = null!; - - private Container mainContent = null!; - private BeatmapCardContentBackground mainContentBackground = null!; private FillFlowContainer statisticsContainer = null!; private FillFlowContainer idleBottomContent = null!; private BeatmapCardDownloadProgressBar downloadProgressBar = null!; - [Resolved] - private OsuColour colours { get; set; } = null!; - [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -94,21 +83,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards Children = new Drawable[] { downloadTracker, - rightAreaBackground = new Container - { - RelativeSizeAxes = Axes.Y, - Width = icon_area_width + 2 * CORNER_RADIUS, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - // workaround for masking artifacts at the top & bottom of card, - // which become especially visible on downloaded beatmaps (when the icon area has a lime background). - Padding = new MarginPadding { Vertical = 1 }, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.White - }, - }, thumbnail = new BeatmapCardThumbnail(beatmapSet) { Name = @"Left (icon) area", @@ -122,61 +96,19 @@ namespace osu.Game.Beatmaps.Drawables.Cards Spacing = new Vector2(1) } }, - new Container + buttonContainer = new CollapsibleButtonContainer(beatmapSet) { - Name = @"Right (button) area", - Width = 30, - RelativeSizeAxes = Axes.Y, - Origin = Anchor.TopRight, - Anchor = Anchor.TopRight, - Padding = new MarginPadding { Vertical = 17.5f }, - Child = rightAreaButtons = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new BeatmapCardIconButton[] - { - new FavouriteButton(beatmapSet) - { - Current = favouriteState, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - }, - new DownloadButton(beatmapSet) - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - State = { BindTarget = downloadTracker.State } - }, - new GoToBeatmapButton(beatmapSet) - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - State = { BindTarget = downloadTracker.State } - } - } - } - }, - mainContent = new Container - { - Name = @"Main content", X = height - CORNER_RADIUS, - Height = height, - CornerRadius = CORNER_RADIUS, - Masking = true, + Width = width - height + CORNER_RADIUS, + FavouriteState = { BindTarget = favouriteState }, + ButtonsCollapsedWidth = CORNER_RADIUS, + ButtonsExpandedWidth = 30, + ButtonsPadding = new MarginPadding { Vertical = 17.5f }, Children = new Drawable[] { - mainContentBackground = new BeatmapCardContentBackground(beatmapSet) - { - RelativeSizeAxes = Axes.Both, - }, new FillFlowContainer { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Horizontal = 10, - Vertical = 4 - }, Direction = FillDirection.Vertical, Children = new Drawable[] { @@ -256,11 +188,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards AutoSizeAxes = Axes.Y, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Padding = new MarginPadding - { - Horizontal = 10, - Vertical = 4 - }, Children = new Drawable[] { idleBottomContent = new FillFlowContainer @@ -388,30 +315,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards { bool showDetails = IsHovered || Expanded.Value; - float targetWidth = width - height; - if (showDetails) - targetWidth = targetWidth - icon_area_width + CORNER_RADIUS; - + buttonContainer.ShowDetails.Value = showDetails; thumbnail.Dimmed.Value = showDetails; // Scale value is intentionally chosen to fit in the spacing of listing displays, as to not overlap horizontally with adjacent cards. // This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left. content.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint); - mainContent.ResizeWidthTo(targetWidth, TRANSITION_DURATION, Easing.OutQuint); - mainContentBackground.Dimmed.Value = showDetails; - statisticsContainer.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); - rightAreaBackground.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, TRANSITION_DURATION, Easing.OutQuint); - rightAreaButtons.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); - - foreach (var button in rightAreaButtons) - { - button.IdleColour = downloadTracker.State.Value != DownloadState.LocallyAvailable ? colourProvider.Light1 : colourProvider.Background3; - button.HoverColour = downloadTracker.State.Value != DownloadState.LocallyAvailable ? colourProvider.Content1 : colourProvider.Foreground1; - } - bool showProgress = downloadTracker.State.Value == DownloadState.Downloading || downloadTracker.State.Value == DownloadState.Importing; idleBottomContent.FadeTo(showProgress ? 0 : 1, TRANSITION_DURATION, Easing.OutQuint); diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index 328294a323..fb498d643a 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -7,11 +7,9 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; -using osu.Game.Beatmaps.Drawables.Cards.Buttons; using osu.Game.Beatmaps.Drawables.Cards.Statistics; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -23,7 +21,6 @@ using osu.Game.Overlays; using osu.Game.Overlays.BeatmapSet; using osuTK; using osu.Game.Resources.Localisation.Web; -using DownloadButton = osu.Game.Beatmaps.Drawables.Cards.Buttons.DownloadButton; namespace osu.Game.Beatmaps.Drawables.Cards { @@ -31,7 +28,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards { private const float width = 475; private const float height = 140; - private const float icon_area_width = 30; public Bindable Expanded { get; } = new BindableBool(); @@ -44,20 +40,13 @@ namespace osu.Game.Beatmaps.Drawables.Cards private readonly BeatmapCardContent content; private BeatmapCardThumbnail thumbnail = null!; + private CollapsibleButtonContainer buttonContainer = null!; - private Container rightAreaBackground = null!; - private Container rightAreaButtons = null!; - - private Container mainContent = null!; - private BeatmapCardContentBackground mainContentBackground = null!; private GridContainer statisticsContainer = null!; private FillFlowContainer idleBottomContent = null!; private BeatmapCardDownloadProgressBar downloadProgressBar = null!; - [Resolved] - private OsuColour colours { get; set; } = null!; - [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -88,21 +77,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards Children = new Drawable[] { downloadTracker, - rightAreaBackground = new Container - { - RelativeSizeAxes = Axes.Y, - Width = icon_area_width + 2 * BeatmapCard.CORNER_RADIUS, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - // workaround for masking artifacts at the top & bottom of card, - // which become especially visible on downloaded beatmaps (when the icon area has a lime background). - Padding = new MarginPadding { Vertical = 1 }, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.White - }, - }, thumbnail = new BeatmapCardThumbnail(beatmapSet) { Name = @"Left (icon) area", @@ -116,61 +90,19 @@ namespace osu.Game.Beatmaps.Drawables.Cards Spacing = new Vector2(1) } }, - new Container + buttonContainer = new CollapsibleButtonContainer(beatmapSet) { - Name = @"Right (button) area", - Width = 30, - RelativeSizeAxes = Axes.Y, - Origin = Anchor.TopRight, - Anchor = Anchor.TopRight, - Padding = new MarginPadding { Vertical = 35 }, - Child = rightAreaButtons = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new BeatmapCardIconButton[] - { - new FavouriteButton(beatmapSet) - { - Current = favouriteState, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - }, - new DownloadButton(beatmapSet) - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - State = { BindTarget = downloadTracker.State } - }, - new GoToBeatmapButton(beatmapSet) - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - State = { BindTarget = downloadTracker.State } - } - } - } - }, - mainContent = new Container - { - Name = @"Main content", X = height - BeatmapCard.CORNER_RADIUS, - Height = height, - CornerRadius = BeatmapCard.CORNER_RADIUS, - Masking = true, + Width = width - height + BeatmapCard.CORNER_RADIUS, + FavouriteState = { BindTarget = favouriteState }, + ButtonsCollapsedWidth = BeatmapCard.CORNER_RADIUS, + ButtonsExpandedWidth = 30, + ButtonsPadding = new MarginPadding { Vertical = 35 }, Children = new Drawable[] { - mainContentBackground = new BeatmapCardContentBackground(beatmapSet) - { - RelativeSizeAxes = Axes.Both, - }, new FillFlowContainer { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Horizontal = 10, - Vertical = 4 - }, Direction = FillDirection.Vertical, Children = new Drawable[] { @@ -248,11 +180,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards AutoSizeAxes = Axes.Y, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Padding = new MarginPadding - { - Horizontal = 10, - Vertical = 4 - }, Children = new Drawable[] { idleBottomContent = new FillFlowContainer @@ -414,28 +341,13 @@ namespace osu.Game.Beatmaps.Drawables.Cards { bool showDetails = IsHovered || Expanded.Value; - float targetWidth = width - height; - if (showDetails) - targetWidth = targetWidth - icon_area_width + BeatmapCard.CORNER_RADIUS; - + buttonContainer.ShowDetails.Value = showDetails; thumbnail.Dimmed.Value = showDetails; // Scale value is intentionally chosen to fit in the spacing of listing displays, as to not overlap horizontally with adjacent cards. // This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left. content.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint); - mainContent.ResizeWidthTo(targetWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - mainContentBackground.Dimmed.Value = showDetails; - - rightAreaBackground.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - rightAreaButtons.FadeTo(showDetails ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - - foreach (var button in rightAreaButtons) - { - button.IdleColour = downloadTracker.State.Value != DownloadState.LocallyAvailable ? colourProvider.Light1 : colourProvider.Background3; - button.HoverColour = downloadTracker.State.Value != DownloadState.LocallyAvailable ? colourProvider.Content1 : colourProvider.Foreground1; - } - bool showProgress = downloadTracker.State.Value == DownloadState.Downloading || downloadTracker.State.Value == DownloadState.Importing; idleBottomContent.FadeTo(showProgress ? 0 : 1, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); diff --git a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs new file mode 100644 index 0000000000..3a2cb80a8d --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs @@ -0,0 +1,184 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps.Drawables.Cards.Buttons; +using osu.Game.Graphics; +using osu.Game.Online; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; + +namespace osu.Game.Beatmaps.Drawables.Cards +{ + public class CollapsibleButtonContainer : Container + { + public Bindable ShowDetails = new Bindable(); + public Bindable FavouriteState = new Bindable(); + + private readonly BeatmapDownloadTracker downloadTracker; + + private float buttonsExpandedWidth; + + public float ButtonsExpandedWidth + { + get => buttonsExpandedWidth; + set + { + buttonsExpandedWidth = value; + buttonArea.Width = value; + if (IsLoaded) + updateState(); + } + } + + private float buttonsCollapsedWidth; + + public float ButtonsCollapsedWidth + { + get => buttonsCollapsedWidth; + set + { + buttonsCollapsedWidth = value; + if (IsLoaded) + updateState(); + } + } + + public MarginPadding ButtonsPadding + { + get => buttons.Padding; + set => buttons.Padding = value; + } + + protected override Container Content => mainContent; + + private readonly Container background; + + private readonly Container buttonArea; + private readonly Container buttons; + + private readonly Container mainArea; + private readonly Container mainContent; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public CollapsibleButtonContainer(APIBeatmapSet beatmapSet) + { + downloadTracker = new BeatmapDownloadTracker(beatmapSet); + + RelativeSizeAxes = Axes.Y; + Masking = true; + CornerRadius = BeatmapCard.CORNER_RADIUS; + + InternalChildren = new Drawable[] + { + downloadTracker, + background = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + // workaround for masking artifacts at the top & bottom of card, + // which become especially visible on downloaded beatmaps (when the icon area has a lime background). + Padding = new MarginPadding { Vertical = 1 }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White + }, + }, + buttonArea = new Container + { + Name = @"Right (button) area", + RelativeSizeAxes = Axes.Y, + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + Child = buttons = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new BeatmapCardIconButton[] + { + new FavouriteButton(beatmapSet) + { + Current = FavouriteState, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }, + new DownloadButton(beatmapSet) + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + State = { BindTarget = downloadTracker.State } + }, + new GoToBeatmapButton(beatmapSet) + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + State = { BindTarget = downloadTracker.State } + } + } + } + }, + mainArea = new Container + { + Name = @"Main content", + RelativeSizeAxes = Axes.Y, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Masking = true, + Children = new Drawable[] + { + new BeatmapCardContentBackground(beatmapSet) + { + RelativeSizeAxes = Axes.Both, + Dimmed = { BindTarget = ShowDetails } + }, + mainContent = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = 10, + Vertical = 4 + }, + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + downloadTracker.State.BindValueChanged(_ => updateState()); + ShowDetails.BindValueChanged(_ => updateState(), true); + FinishTransforms(true); + } + + private void updateState() + { + float targetWidth = Width - (ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth); + + mainArea.ResizeWidthTo(targetWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + background.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + buttons.FadeTo(ShowDetails.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + foreach (var button in buttons) + { + button.IdleColour = downloadTracker.State.Value != DownloadState.LocallyAvailable ? colourProvider.Light1 : colourProvider.Background3; + button.HoverColour = downloadTracker.State.Value != DownloadState.LocallyAvailable ? colourProvider.Content1 : colourProvider.Foreground1; + } + } + } +} From d6f60399342512250fb0c92b90d0c8e13d853eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Dec 2021 13:27:11 +0100 Subject: [PATCH 384/419] Extract base class for beatmap cards --- .../Beatmaps/Drawables/Cards/BeatmapCard.cs | 101 +++++----------- .../Drawables/Cards/BeatmapCardBase.cs | 80 +++++++++++++ .../Drawables/Cards/BeatmapCardContent.cs | 14 +-- .../Cards/BeatmapCardContentBackground.cs | 4 +- .../Cards/BeatmapCardDownloadProgressBar.cs | 4 +- .../Drawables/Cards/BeatmapCardExtra.cs | 108 ++++++------------ .../Drawables/Cards/BeatmapCardThumbnail.cs | 4 +- .../Cards/Buttons/BeatmapCardIconButton.cs | 2 +- .../Drawables/Cards/Buttons/DownloadButton.cs | 4 +- .../Cards/Buttons/GoToBeatmapButton.cs | 2 +- .../Drawables/Cards/Buttons/PlayButton.cs | 2 +- .../Cards/CollapsibleButtonContainer.cs | 10 +- 12 files changed, 168 insertions(+), 167 deletions(-) create mode 100644 osu.Game/Beatmaps/Drawables/Cards/BeatmapCardBase.cs diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 13c4cfe207..b0df2e40d3 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -5,18 +5,14 @@ using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps.Drawables.Cards.Statistics; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapSet; @@ -25,21 +21,14 @@ using osu.Game.Resources.Localisation.Web; namespace osu.Game.Beatmaps.Drawables.Cards { - public class BeatmapCard : OsuClickableContainer + public class BeatmapCard : BeatmapCardBase { - public const float TRANSITION_DURATION = 400; - public const float CORNER_RADIUS = 10; - - public IBindable Expanded { get; } + protected override Drawable IdleContent => idleBottomContent; + protected override Drawable DownloadInProgressContent => downloadProgressBar; private const float width = 408; private const float height = 100; - private readonly APIBeatmapSet beatmapSet; - private readonly Bindable favouriteState; - - private readonly BeatmapDownloadTracker downloadTracker; - [Cached] private readonly BeatmapCardContent content; @@ -55,18 +44,13 @@ namespace osu.Game.Beatmaps.Drawables.Cards private OverlayColourProvider colourProvider { get; set; } = null!; public BeatmapCard(APIBeatmapSet beatmapSet, bool allowExpansion = true) - : base(HoverSampleSet.Submit) + : base(beatmapSet, allowExpansion) { - Expanded = new BindableBool { Disabled = !allowExpansion }; - - this.beatmapSet = beatmapSet; - favouriteState = new Bindable(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount)); - downloadTracker = new BeatmapDownloadTracker(beatmapSet); content = new BeatmapCardContent(height); } - [BackgroundDependencyLoader(true)] - private void load(BeatmapSetOverlay? beatmapSetOverlay) + [BackgroundDependencyLoader] + private void load() { Width = width; Height = height; @@ -75,15 +59,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards GridContainer titleContainer = null!; GridContainer artistContainer = null!; - InternalChild = content.With(c => + Child = content.With(c => { c.MainContent = new Container { RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - downloadTracker, - thumbnail = new BeatmapCardThumbnail(beatmapSet) + thumbnail = new BeatmapCardThumbnail(BeatmapSet) { Name = @"Left (icon) area", Size = new Vector2(height), @@ -96,11 +79,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards Spacing = new Vector2(1) } }, - buttonContainer = new CollapsibleButtonContainer(beatmapSet) + buttonContainer = new CollapsibleButtonContainer(BeatmapSet) { X = height - CORNER_RADIUS, Width = width - height + CORNER_RADIUS, - FavouriteState = { BindTarget = favouriteState }, + FavouriteState = { BindTarget = FavouriteState }, ButtonsCollapsedWidth = CORNER_RADIUS, ButtonsExpandedWidth = 30, ButtonsPadding = new MarginPadding { Vertical = 17.5f }, @@ -131,7 +114,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { new OsuSpriteText { - Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title), + Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, Truncate = true @@ -177,7 +160,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards d.AutoSizeAxes = Axes.Both; d.Margin = new MarginPadding { Top = 2 }; d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); - d.AddUserLink(beatmapSet.Author); + d.AddUserLink(BeatmapSet.Author); }), } }, @@ -209,7 +192,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards AlwaysPresent = true, ChildrenEnumerable = createStatistics() }, - new BeatmapCardExtraInfoRow(beatmapSet) + new BeatmapCardExtraInfoRow(BeatmapSet) } }, downloadProgressBar = new BeatmapCardDownloadProgressBar @@ -218,8 +201,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards Height = 6, Anchor = Anchor.Centre, Origin = Anchor.Centre, - State = { BindTarget = downloadTracker.State }, - Progress = { BindTarget = downloadTracker.Progress } + State = { BindTarget = DownloadTracker.State }, + Progress = { BindTarget = DownloadTracker.Progress } } } } @@ -232,18 +215,18 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Horizontal = 10, Vertical = 13 }, - Child = new BeatmapCardDifficultyList(beatmapSet) + Child = new BeatmapCardDifficultyList(BeatmapSet) }; c.Expanded.BindTarget = Expanded; }); - if (beatmapSet.HasVideo) + if (BeatmapSet.HasVideo) leftIconArea.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) }); - if (beatmapSet.HasStoryboard) + if (BeatmapSet.HasStoryboard) leftIconArea.Add(new IconPill(FontAwesome.Solid.Image) { IconSize = new Vector2(20) }); - if (beatmapSet.HasExplicitContent) + if (BeatmapSet.HasExplicitContent) { titleContainer.Content[0][1] = new ExplicitContentBeatmapPill { @@ -253,7 +236,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards }; } - if (beatmapSet.TrackId != null) + if (BeatmapSet.TrackId != null) { artistContainer.Content[0][1] = new FeaturedArtistBeatmapPill { @@ -262,57 +245,36 @@ namespace osu.Game.Beatmaps.Drawables.Cards Margin = new MarginPadding { Left = 5 } }; } - - Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapSet.OnlineID); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - downloadTracker.State.BindValueChanged(_ => updateState()); - Expanded.BindValueChanged(_ => updateState(), true); - FinishTransforms(true); - } - - protected override bool OnHover(HoverEvent e) - { - updateState(); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateState(); - base.OnHoverLost(e); } private LocalisableString createArtistText() { - var romanisableArtist = new RomanisableString(beatmapSet.ArtistUnicode, beatmapSet.Artist); + var romanisableArtist = new RomanisableString(BeatmapSet.ArtistUnicode, BeatmapSet.Artist); return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist); } private IEnumerable createStatistics() { - var hypesStatistic = HypesStatistic.CreateFor(beatmapSet); + var hypesStatistic = HypesStatistic.CreateFor(BeatmapSet); if (hypesStatistic != null) yield return hypesStatistic; - var nominationsStatistic = NominationsStatistic.CreateFor(beatmapSet); + var nominationsStatistic = NominationsStatistic.CreateFor(BeatmapSet); if (nominationsStatistic != null) yield return nominationsStatistic; - yield return new FavouritesStatistic(beatmapSet) { Current = favouriteState }; - yield return new PlayCountStatistic(beatmapSet); + yield return new FavouritesStatistic(BeatmapSet) { Current = FavouriteState }; + yield return new PlayCountStatistic(BeatmapSet); - var dateStatistic = BeatmapCardDateStatistic.CreateFor(beatmapSet); + var dateStatistic = BeatmapCardDateStatistic.CreateFor(BeatmapSet); if (dateStatistic != null) yield return dateStatistic; } - private void updateState() + protected override void UpdateState() { + base.UpdateState(); + bool showDetails = IsHovered || Expanded.Value; buttonContainer.ShowDetails.Value = showDetails; @@ -323,11 +285,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards content.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint); statisticsContainer.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); - - bool showProgress = downloadTracker.State.Value == DownloadState.Downloading || downloadTracker.State.Value == DownloadState.Importing; - - idleBottomContent.FadeTo(showProgress ? 0 : 1, TRANSITION_DURATION, Easing.OutQuint); - downloadProgressBar.FadeTo(showProgress ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardBase.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardBase.cs new file mode 100644 index 0000000000..77df0809bc --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardBase.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; + +namespace osu.Game.Beatmaps.Drawables.Cards +{ + public abstract class BeatmapCardBase : OsuClickableContainer + { + public const float TRANSITION_DURATION = 400; + public const float CORNER_RADIUS = 10; + + public IBindable Expanded { get; } + + protected readonly APIBeatmapSet BeatmapSet; + protected readonly Bindable FavouriteState; + + protected abstract Drawable IdleContent { get; } + protected abstract Drawable DownloadInProgressContent { get; } + + protected readonly BeatmapDownloadTracker DownloadTracker; + + protected BeatmapCardBase(APIBeatmapSet beatmapSet, bool allowExpansion = true) + : base(HoverSampleSet.Submit) + { + Expanded = new BindableBool { Disabled = !allowExpansion }; + + BeatmapSet = beatmapSet; + FavouriteState = new Bindable(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount)); + DownloadTracker = new BeatmapDownloadTracker(beatmapSet); + } + + [BackgroundDependencyLoader(true)] + private void load(BeatmapSetOverlay? beatmapSetOverlay) + { + Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID); + + AddInternal(DownloadTracker); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + DownloadTracker.State.BindValueChanged(_ => UpdateState()); + Expanded.BindValueChanged(_ => UpdateState(), true); + FinishTransforms(true); + } + + protected override bool OnHover(HoverEvent e) + { + UpdateState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + UpdateState(); + base.OnHoverLost(e); + } + + protected virtual void UpdateState() + { + bool showProgress = DownloadTracker.State.Value == DownloadState.Downloading || DownloadTracker.State.Value == DownloadState.Importing; + + IdleContent.FadeTo(showProgress ? 0 : 1, TRANSITION_DURATION, Easing.OutQuint); + DownloadInProgressContent.FadeTo(showProgress ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs index 286e03e700..e16421503c 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs @@ -54,7 +54,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - CornerRadius = BeatmapCard.CORNER_RADIUS, + CornerRadius = BeatmapCardBase.CORNER_RADIUS, Masking = true, Unhovered = _ => updateFromHoverChange(), Children = new Drawable[] @@ -67,7 +67,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { RelativeSizeAxes = Axes.X, Height = height, - CornerRadius = BeatmapCard.CORNER_RADIUS, + CornerRadius = BeatmapCardBase.CORNER_RADIUS, Masking = true, }, dropdownContent = new HoverHandlingContainer @@ -91,7 +91,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards borderContainer = new Container { RelativeSizeAxes = Axes.Both, - CornerRadius = BeatmapCard.CORNER_RADIUS, + CornerRadius = BeatmapCardBase.CORNER_RADIUS, Masking = true, BorderThickness = 3, Child = new Box @@ -139,9 +139,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards private void updateState() { - background.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - dropdownContent.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - borderContainer.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + background.FadeTo(Expanded.Value ? 1 : 0, BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); + dropdownContent.FadeTo(Expanded.Value ? 1 : 0, BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); + borderContainer.FadeTo(Expanded.Value ? 1 : 0, BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); content.TweenEdgeEffectTo(new EdgeEffectParameters { @@ -150,7 +150,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards Radius = 10, Colour = Colour4.Black.Opacity(Expanded.Value ? 0.3f : 0f), Hollow = true, - }, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + }, BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); } private class ExpandedContentScrollContainer : OsuScrollContainer diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs index 392f5d1bfa..6388e1698c 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs @@ -62,10 +62,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards private void updateState() => Schedule(() => { - background.FadeColour(Dimmed.Value ? colourProvider.Background4 : colourProvider.Background2, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + background.FadeColour(Dimmed.Value ? colourProvider.Background4 : colourProvider.Background2, BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); var gradient = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0), Colour4.White.Opacity(0.2f)); - cover.FadeColour(gradient, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + cover.FadeColour(gradient, BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); }); } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDownloadProgressBar.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDownloadProgressBar.cs index ffb4e0c540..b80c5221ab 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDownloadProgressBar.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDownloadProgressBar.cs @@ -82,14 +82,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards break; case DownloadState.Importing: - foregroundFill.FadeColour(colours.Yellow, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + foregroundFill.FadeColour(colours.Yellow, BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); break; } } private void progressChanged() { - foreground.ResizeWidthTo((float)progress.Value, progress.Value > 0 ? BeatmapCard.TRANSITION_DURATION : 0, Easing.OutQuint); + foreground.ResizeWidthTo((float)progress.Value, progress.Value > 0 ? BeatmapCardBase.TRANSITION_DURATION : 0, Easing.OutQuint); } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index fb498d643a..39acbb352d 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -4,18 +4,14 @@ #nullable enable using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps.Drawables.Cards.Statistics; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapSet; @@ -24,18 +20,14 @@ using osu.Game.Resources.Localisation.Web; namespace osu.Game.Beatmaps.Drawables.Cards { - public class BeatmapCardExtra : OsuClickableContainer + public class BeatmapCardExtra : BeatmapCardBase { + protected override Drawable IdleContent => idleBottomContent; + protected override Drawable DownloadInProgressContent => downloadProgressBar; + private const float width = 475; private const float height = 140; - public Bindable Expanded { get; } = new BindableBool(); - - private readonly APIBeatmapSet beatmapSet; - private readonly Bindable favouriteState; - - private readonly BeatmapDownloadTracker downloadTracker; - [Cached] private readonly BeatmapCardContent content; @@ -50,12 +42,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public BeatmapCardExtra(APIBeatmapSet beatmapSet) - : base(HoverSampleSet.Submit) + public BeatmapCardExtra(APIBeatmapSet beatmapSet, bool allowExpansion = true) + : base(beatmapSet, allowExpansion) { - this.beatmapSet = beatmapSet; - favouriteState = new Bindable(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount)); - downloadTracker = new BeatmapDownloadTracker(beatmapSet); content = new BeatmapCardContent(height); } @@ -69,19 +58,18 @@ namespace osu.Game.Beatmaps.Drawables.Cards GridContainer titleContainer = null!; GridContainer artistContainer = null!; - InternalChild = content.With(c => + Child = content.With(c => { c.MainContent = new Container { RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - downloadTracker, - thumbnail = new BeatmapCardThumbnail(beatmapSet) + thumbnail = new BeatmapCardThumbnail(BeatmapSet) { Name = @"Left (icon) area", Size = new Vector2(height), - Padding = new MarginPadding { Right = BeatmapCard.CORNER_RADIUS }, + Padding = new MarginPadding { Right = CORNER_RADIUS }, Child = leftIconArea = new FillFlowContainer { Margin = new MarginPadding(5), @@ -90,12 +78,12 @@ namespace osu.Game.Beatmaps.Drawables.Cards Spacing = new Vector2(1) } }, - buttonContainer = new CollapsibleButtonContainer(beatmapSet) + buttonContainer = new CollapsibleButtonContainer(BeatmapSet) { - X = height - BeatmapCard.CORNER_RADIUS, - Width = width - height + BeatmapCard.CORNER_RADIUS, - FavouriteState = { BindTarget = favouriteState }, - ButtonsCollapsedWidth = BeatmapCard.CORNER_RADIUS, + X = height - CORNER_RADIUS, + Width = width - height + CORNER_RADIUS, + FavouriteState = { BindTarget = FavouriteState }, + ButtonsCollapsedWidth = CORNER_RADIUS, ButtonsExpandedWidth = 30, ButtonsPadding = new MarginPadding { Vertical = 35 }, Children = new Drawable[] @@ -125,7 +113,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { new OsuSpriteText { - Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title), + Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, Truncate = true @@ -166,7 +154,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { RelativeSizeAxes = Axes.X, Truncate = true, - Text = beatmapSet.Source, + Text = BeatmapSet.Source, Shadow = false, Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), Colour = colourProvider.Content2 @@ -200,7 +188,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards d.AutoSizeAxes = Axes.Both; d.Margin = new MarginPadding { Top = 2 }; d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); - d.AddUserLink(beatmapSet.Author); + d.AddUserLink(BeatmapSet.Author); }), statisticsContainer = new GridContainer { @@ -223,7 +211,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards new Drawable[3] } }, - new BeatmapCardExtraInfoRow(beatmapSet) + new BeatmapCardExtraInfoRow(BeatmapSet) } }, downloadProgressBar = new BeatmapCardDownloadProgressBar @@ -232,8 +220,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards Height = 6, Anchor = Anchor.Centre, Origin = Anchor.Centre, - State = { BindTarget = downloadTracker.State }, - Progress = { BindTarget = downloadTracker.Progress } + State = { BindTarget = DownloadTracker.State }, + Progress = { BindTarget = DownloadTracker.Progress } } } } @@ -246,18 +234,18 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Horizontal = 10, Vertical = 13 }, - Child = new BeatmapCardDifficultyList(beatmapSet) + Child = new BeatmapCardDifficultyList(BeatmapSet) }; c.Expanded.BindTarget = Expanded; }); - if (beatmapSet.HasVideo) + if (BeatmapSet.HasVideo) leftIconArea.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) }); - if (beatmapSet.HasStoryboard) + if (BeatmapSet.HasStoryboard) leftIconArea.Add(new IconPill(FontAwesome.Solid.Image) { IconSize = new Vector2(20) }); - if (beatmapSet.HasExplicitContent) + if (BeatmapSet.HasExplicitContent) { titleContainer.Content[0][1] = new ExplicitContentBeatmapPill { @@ -267,7 +255,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards }; } - if (beatmapSet.TrackId != null) + if (BeatmapSet.TrackId != null) { artistContainer.Content[0][1] = new FeaturedArtistBeatmapPill { @@ -279,33 +267,12 @@ namespace osu.Game.Beatmaps.Drawables.Cards createStatistics(); - Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapSet.OnlineID); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - downloadTracker.State.BindValueChanged(_ => updateState()); - Expanded.BindValueChanged(_ => updateState(), true); - FinishTransforms(true); - } - - protected override bool OnHover(HoverEvent e) - { - updateState(); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateState(); - base.OnHoverLost(e); + Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID); } private LocalisableString createArtistText() { - var romanisableArtist = new RomanisableString(beatmapSet.ArtistUnicode, beatmapSet.Artist); + var romanisableArtist = new RomanisableString(BeatmapSet.ArtistUnicode, BeatmapSet.Artist); return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist); } @@ -317,28 +284,30 @@ namespace osu.Game.Beatmaps.Drawables.Cards return original; } - statisticsContainer.Content[0][0] = withMargin(new FavouritesStatistic(beatmapSet) + statisticsContainer.Content[0][0] = withMargin(new FavouritesStatistic(BeatmapSet) { - Current = favouriteState, + Current = FavouriteState, }); - statisticsContainer.Content[1][0] = withMargin(new PlayCountStatistic(beatmapSet)); + statisticsContainer.Content[1][0] = withMargin(new PlayCountStatistic(BeatmapSet)); - var hypesStatistic = HypesStatistic.CreateFor(beatmapSet); + var hypesStatistic = HypesStatistic.CreateFor(BeatmapSet); if (hypesStatistic != null) statisticsContainer.Content[0][1] = withMargin(hypesStatistic); - var nominationsStatistic = NominationsStatistic.CreateFor(beatmapSet); + var nominationsStatistic = NominationsStatistic.CreateFor(BeatmapSet); if (nominationsStatistic != null) statisticsContainer.Content[1][1] = withMargin(nominationsStatistic); - var dateStatistic = BeatmapCardDateStatistic.CreateFor(beatmapSet); + var dateStatistic = BeatmapCardDateStatistic.CreateFor(BeatmapSet); if (dateStatistic != null) statisticsContainer.Content[0][2] = withMargin(dateStatistic); } - private void updateState() + protected override void UpdateState() { + base.UpdateState(); + bool showDetails = IsHovered || Expanded.Value; buttonContainer.ShowDetails.Value = showDetails; @@ -347,11 +316,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards // Scale value is intentionally chosen to fit in the spacing of listing displays, as to not overlap horizontally with adjacent cards. // This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left. content.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint); - - bool showProgress = downloadTracker.State.Value == DownloadState.Downloading || downloadTracker.State.Value == DownloadState.Importing; - - idleBottomContent.FadeTo(showProgress ? 0 : 1, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - downloadProgressBar.FadeTo(showProgress ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs index f11a5916e1..e6b305552a 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -88,8 +88,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards { bool shouldDim = Dimmed.Value || playButton.Playing.Value; - playButton.FadeTo(shouldDim ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - cover.FadeColour(shouldDim ? OsuColour.Gray(0.2f) : Color4.White, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + playButton.FadeTo(shouldDim ? 1 : 0, BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); + cover.FadeColour(shouldDim ? OsuColour.Gray(0.2f) : Color4.White, BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs index e362e3abeb..02c9ea640a 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs @@ -115,7 +115,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons bool isHovered = IsHovered && Enabled.Value; content.ScaleTo(isHovered ? 1.2f : 1, 500, Easing.OutQuint); - content.FadeColour(isHovered ? HoverColour : IdleColour, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + content.FadeColour(isHovered ? HoverColour : IdleColour, BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/DownloadButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/DownloadButton.cs index c94e335e8f..d7ba4af21e 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/DownloadButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/DownloadButton.cs @@ -69,7 +69,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons case DownloadState.LocallyAvailable: Action = null; TooltipText = string.Empty; - this.FadeOut(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + this.FadeOut(BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); break; case DownloadState.NotDownloaded: @@ -81,7 +81,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons } Action = () => beatmaps.Download(beatmapSet, preferNoVideo.Value); - this.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + this.FadeIn(BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); spinner.Hide(); Icon.Show(); diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs index 9a6a3c01b7..b039eb6f10 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs @@ -43,7 +43,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons private void updateState() { - this.FadeTo(state.Value == DownloadState.LocallyAvailable ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + this.FadeTo(state.Value == DownloadState.LocallyAvailable ? 1 : 0, BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs index f7bab26666..6d66d5001a 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs @@ -141,7 +141,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons private void toggleLoading(bool loading) { Enabled.Value = !loading; - icon.FadeTo(loading ? 0 : 1, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + icon.FadeTo(loading ? 0 : 1, BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); loadingSpinner.State.Value = loading ? Visibility.Visible : Visibility.Hidden; } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs index 3a2cb80a8d..e1a630182f 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs @@ -78,7 +78,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.Y; Masking = true; - CornerRadius = BeatmapCard.CORNER_RADIUS; + CornerRadius = BeatmapCardBase.CORNER_RADIUS; InternalChildren = new Drawable[] { @@ -133,7 +133,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Name = @"Main content", RelativeSizeAxes = Axes.Y, - CornerRadius = BeatmapCard.CORNER_RADIUS, + CornerRadius = BeatmapCardBase.CORNER_RADIUS, Masking = true, Children = new Drawable[] { @@ -169,10 +169,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards { float targetWidth = Width - (ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth); - mainArea.ResizeWidthTo(targetWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + mainArea.ResizeWidthTo(targetWidth, BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); - background.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - buttons.FadeTo(ShowDetails.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + background.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); + buttons.FadeTo(ShowDetails.Value ? 1 : 0, BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); foreach (var button in buttons) { From 33e930f47786c5edad0d003fdee13aaf10f079cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Dec 2021 13:29:20 +0100 Subject: [PATCH 385/419] Move scale-on-expand logic to `BeatmapCardContent` --- osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs | 4 ---- osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs | 4 ++++ osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs | 4 ---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index b0df2e40d3..a3b4c4c86c 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -280,10 +280,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards buttonContainer.ShowDetails.Value = showDetails; thumbnail.Dimmed.Value = showDetails; - // Scale value is intentionally chosen to fit in the spacing of listing displays, as to not overlap horizontally with adjacent cards. - // This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left. - content.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint); - statisticsContainer.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs index e16421503c..9c43c16100 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs @@ -139,6 +139,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards private void updateState() { + // Scale value is intentionally chosen to fit in the spacing of listing displays, as to not overlap horizontally with adjacent cards. + // This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left. + this.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint); + background.FadeTo(Expanded.Value ? 1 : 0, BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); dropdownContent.FadeTo(Expanded.Value ? 1 : 0, BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); borderContainer.FadeTo(Expanded.Value ? 1 : 0, BeatmapCardBase.TRANSITION_DURATION, Easing.OutQuint); diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index 39acbb352d..308cba9f0f 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -312,10 +312,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards buttonContainer.ShowDetails.Value = showDetails; thumbnail.Dimmed.Value = showDetails; - - // Scale value is intentionally chosen to fit in the spacing of listing displays, as to not overlap horizontally with adjacent cards. - // This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left. - content.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint); } } } From 04cfae9bdeb525855a34527a217e8b2cba34b115 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 18 Dec 2021 16:26:15 +0900 Subject: [PATCH 386/419] Fix "Random Skin" text not showing up correctly --- osu.Game/Database/RealmLiveUnmanaged.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Database/RealmLiveUnmanaged.cs b/osu.Game/Database/RealmLiveUnmanaged.cs index 5a69898206..ea50ccc1ff 100644 --- a/osu.Game/Database/RealmLiveUnmanaged.cs +++ b/osu.Game/Database/RealmLiveUnmanaged.cs @@ -26,6 +26,8 @@ namespace osu.Game.Database public bool Equals(ILive? other) => ID == other?.ID; + public override string ToString() => Value.ToString(); + public Guid ID => Value.ID; public void PerformRead(Action perform) => perform(Value); From c11217755987abbe9ca0c63ad832f6839463647e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 19 Dec 2021 12:48:09 +0100 Subject: [PATCH 387/419] Bring profile header test in line with modern conventions - Removed online code that didn't work anyway after the introduction of the development web instance. - Removed some weird test steps. - Fixed online/offline test steps not working at all due to identical user ID. --- .../Online/TestSceneUserProfileHeader.cs | 73 +++++-------------- 1 file changed, 20 insertions(+), 53 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs index 76997bded7..c319d2f7de 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs @@ -2,85 +2,52 @@ // See the LICENCE file in the repository root for full licence text. using System; +using NUnit.Framework; using osu.Framework.Allocation; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; +using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Profile; -using osu.Game.Users; namespace osu.Game.Tests.Visual.Online { public class TestSceneUserProfileHeader : OsuTestScene { - protected override bool UseOnlineAPI => true; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); - [Resolved] - private IAPIProvider api { get; set; } + private ProfileHeader header; - private readonly ProfileHeader header; - - public TestSceneUserProfileHeader() + [SetUpSteps] + public void SetUpSteps() { - header = new ProfileHeader(); - Add(header); + AddStep("create header", () => Child = header = new ProfileHeader()); + } - AddStep("Show test dummy", () => header.User.Value = TestSceneUserProfileOverlay.TEST_USER); + [Test] + public void TestBasic() + { + AddStep("Show example user", () => header.User.Value = TestSceneUserProfileOverlay.TEST_USER); + } - AddStep("Show null dummy", () => header.User.Value = new APIUser - { - Username = "Null" - }); - - AddStep("Show online dummy", () => header.User.Value = new APIUser + [Test] + public void TestOnlineState() + { + AddStep("Show online user", () => header.User.Value = new APIUser { + Id = 1001, Username = "IAmOnline", LastVisit = DateTimeOffset.Now, IsOnline = true, }); - AddStep("Show offline dummy", () => header.User.Value = new APIUser + AddStep("Show offline user", () => header.User.Value = new APIUser { + Id = 1002, Username = "IAmOffline", - LastVisit = DateTimeOffset.Now, + LastVisit = DateTimeOffset.Now.AddDays(-10), IsOnline = false, }); - - addOnlineStep("Show ppy", new APIUser - { - Username = @"peppy", - Id = 2, - IsSupporter = true, - Country = new Country { FullName = @"Australia", FlagName = @"AU" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg" - }); - - addOnlineStep("Show flyte", new APIUser - { - Username = @"flyte", - Id = 3103765, - Country = new Country { FullName = @"Japan", FlagName = @"JP" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" - }); - } - - private void addOnlineStep(string name, APIUser fallback) - { - AddStep(name, () => - { - if (api.IsLoggedIn) - { - var request = new GetUserRequest(fallback.Id); - request.Success += user => header.User.Value = user; - api.Queue(request); - } - else - header.User.Value = fallback; - }); } } } From 097402677d131f96aca3031c13876a5d927bcfb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 19 Dec 2021 12:55:24 +0100 Subject: [PATCH 388/419] Add test for ranked/unranked user display --- .../Online/TestSceneUserProfileHeader.cs | 39 +++++++++++++++++++ .../Online/TestSceneUserProfileOverlay.cs | 1 + osu.Game/Users/UserStatistics.cs | 3 ++ 3 files changed, 43 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs index c319d2f7de..dda9543159 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Profile; +using osu.Game.Users; namespace osu.Game.Tests.Visual.Online { @@ -49,5 +51,42 @@ namespace osu.Game.Tests.Visual.Online IsOnline = false, }); } + + [Test] + public void TestRankedState() + { + AddStep("Show ranked user", () => header.User.Value = new APIUser + { + Id = 2001, + Username = "RankedUser", + Statistics = new UserStatistics + { + IsRanked = true, + GlobalRank = 15000, + CountryRank = 1500, + RankHistory = new APIRankHistory + { + Mode = @"osu", + Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray() + }, + } + }); + + AddStep("Show unranked user", () => header.User.Value = new APIUser + { + Id = 2002, + Username = "UnrankedUser", + Statistics = new UserStatistics + { + IsRanked = false, + // web will sometimes return non-empty rank history even for unranked users. + RankHistory = new APIRankHistory + { + Mode = @"osu", + Data = Enumerable.Range(2345, 85).ToArray() + }, + } + }); + } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index 1c92bb1e38..78e2ceb45b 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -29,6 +29,7 @@ namespace osu.Game.Tests.Visual.Online ProfileOrder = new[] { "me" }, Statistics = new UserStatistics { + IsRanked = true, GlobalRank = 2148, CountryRank = 1, PP = 4567.89m, diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index c690447256..f8d26fe421 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -27,6 +27,9 @@ namespace osu.Game.Users public int Progress; } + [JsonProperty(@"is_ranked")] + public bool IsRanked; + [JsonProperty(@"global_rank")] public int? GlobalRank; From a6ccbafc77ead8416a8ea6f10407bd30db186046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 19 Dec 2021 13:19:00 +0100 Subject: [PATCH 389/419] Fix rank graph showing for unranked users --- osu.Game/Overlays/Profile/Header/Components/RankGraph.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs index d6e515d8a1..d195babcbf 100644 --- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs +++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs @@ -42,7 +42,9 @@ namespace osu.Game.Overlays.Profile.Header.Components private void updateStatistics(UserStatistics statistics) { - int[] userRanks = statistics?.RankHistory?.Data; + // checking both IsRanked and RankHistory is required. + // see https://github.com/ppy/osu-web/blob/154ceafba0f35a1dd935df53ec98ae2ea5615f9f/resources/assets/lib/profile-page/rank-chart.tsx#L46 + int[] userRanks = statistics?.IsRanked == true ? statistics.RankHistory?.Data : null; Data = userRanks?.Select((x, index) => new KeyValuePair(index, x)).Where(x => x.Value != 0).ToArray(); } From 1203ae5984ca1c399f5c876a2cee6b4900466e5f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 20 Dec 2021 11:49:39 +0900 Subject: [PATCH 390/419] Require installing .NET 5 in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 24b70b2de6..f18c5e76f9 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ You can see some examples of custom rulesets by visiting the [custom ruleset dir Please make sure you have the following prerequisites: -- A desktop platform with the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) or higher installed. +- A desktop platform with the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) installed. - When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/). - When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/). - When running on Linux, please have a system-wide FFmpeg installation available to support video decoding. From 11f3ec0cb5821311ae5b5f8df1231925c3bd7efe Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 20 Dec 2021 12:44:22 +0900 Subject: [PATCH 391/419] Wait for previous screen to be hidden in test --- .../Visual/Background/TestSceneBackgroundScreenDefault.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs index 3211405670..844fe7705a 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs @@ -85,6 +85,7 @@ namespace osu.Game.Tests.Visual.Background // of note, this needs to be a type that doesn't match BackgroundScreenDefault else it is silently not pushed by the background stack. AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value))); AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen()); + AddUntilStep("previous background hidden", () => !screen.IsAlive); AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null); From 9316abc2786dded8078cccb977d21a7058c5faa4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 20 Dec 2021 13:10:13 +0900 Subject: [PATCH 392/419] Better fix for intermittent multiplayer tests --- .../Visual/Multiplayer/QueueModeTestScene.cs | 3 +-- .../Multiplayer/TestSceneMultiplayer.cs | 10 ++++---- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 2 +- .../Multiplayer/MultiplayerTestScene.cs | 2 +- .../Multiplayer/TestMultiplayerClient.cs | 12 +++++++--- .../Multiplayer/TestMultiplayerRoomManager.cs | 24 ++----------------- 6 files changed, 19 insertions(+), 34 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 357db16e2c..83292c36bf 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -41,7 +41,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerScreenStack multiplayerScreenStack; protected TestMultiplayerClient Client => multiplayerScreenStack.Client; - protected TestMultiplayerRoomManager RoomManager => multiplayerScreenStack.RoomManager; [Cached(typeof(UserLookupCache))] private UserLookupCache lookupCache = new TestUserLookupCache(); @@ -93,7 +92,7 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); - AddUntilStep("wait for join", () => RoomManager.RoomJoined); + AddUntilStep("wait for join", () => Client.RoomJoined); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 710855a605..99411254a7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -216,7 +216,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("Press select", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - AddUntilStep("wait for join", () => roomManager.RoomJoined); + AddUntilStep("wait for join", () => client.RoomJoined); } [Test] @@ -295,7 +295,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join room", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - AddUntilStep("wait for join", () => roomManager.RoomJoined); + AddUntilStep("wait for join", () => client.RoomJoined); AddAssert("Check participant count correct", () => client.APIRoom?.ParticipantCount.Value == 1); AddAssert("Check participant list contains user", () => client.APIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1); @@ -353,7 +353,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick()); AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - AddUntilStep("wait for join", () => roomManager.RoomJoined); + AddUntilStep("wait for join", () => client.RoomJoined); } [Test] @@ -646,7 +646,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join room", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - AddUntilStep("wait for join", () => roomManager.RoomJoined); + AddUntilStep("wait for join", () => client.RoomJoined); AddAssert("local room has correct settings", () => { @@ -838,7 +838,7 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); - AddUntilStep("wait for join", () => roomManager.RoomJoined); + AddUntilStep("wait for join", () => client.RoomJoined); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index ccce26ad31..4ecbc30d76 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); - AddUntilStep("wait for join", () => multiplayerScreenStack.RoomManager.RoomJoined); + AddUntilStep("wait for join", () => client.RoomJoined); } } } diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 5656704abf..7607122ef0 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer protected new MultiplayerTestSceneDependencies OnlinePlayDependencies => (MultiplayerTestSceneDependencies)base.OnlinePlayDependencies; - public bool RoomJoined => RoomManager.RoomJoined; + public bool RoomJoined => Client.RoomJoined; private readonly bool joinRoom; diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 4e0cfe405e..5b08b6b835 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -31,9 +31,10 @@ namespace osu.Game.Tests.Visual.Multiplayer private readonly Bindable isConnected = new Bindable(true); public new Room? APIRoom => base.APIRoom; - public Action? RoomSetupAction; + public bool RoomJoined { get; private set; } + [Resolved] private IAPIProvider api { get; set; } = null!; @@ -49,7 +50,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private MultiplayerPlaylistItem? currentItem => Room?.Playlist[currentIndex]; private int currentIndex; - private long lastPlaylistItemId; public TestMultiplayerClient(TestMultiplayerRoomManager roomManager) @@ -217,9 +217,15 @@ namespace osu.Game.Tests.Visual.Multiplayer // emulate the server sending this after the join room. scheduler required to make sure the join room event is fired first (in Join). changeMatchType(Room.Settings.MatchType).Wait(); + + RoomJoined = true; } - protected override Task LeaveRoomInternal() => Task.CompletedTask; + protected override Task LeaveRoomInternal() + { + RoomJoined = false; + return Task.CompletedTask; + } public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs index a1f010f082..296db3152d 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs @@ -17,8 +17,6 @@ namespace osu.Game.Tests.Visual.Multiplayer ///
public class TestMultiplayerRoomManager : MultiplayerRoomManager { - public bool RoomJoined { get; private set; } - private readonly TestRoomRequestsHandler requestsHandler; public TestMultiplayerRoomManager(TestRoomRequestsHandler requestsHandler) @@ -29,28 +27,10 @@ namespace osu.Game.Tests.Visual.Multiplayer public IReadOnlyList ServerSideRooms => requestsHandler.ServerSideRooms; public override void CreateRoom(Room room, Action onSuccess = null, Action onError = null) - { - base.CreateRoom(room, r => - { - onSuccess?.Invoke(r); - RoomJoined = true; - }, onError); - } + => base.CreateRoom(room, r => onSuccess?.Invoke(r), onError); public override void JoinRoom(Room room, string password = null, Action onSuccess = null, Action onError = null) - { - base.JoinRoom(room, password, r => - { - onSuccess?.Invoke(r); - RoomJoined = true; - }, onError); - } - - public override void PartRoom() - { - base.PartRoom(); - RoomJoined = false; - } + => base.JoinRoom(room, password, r => onSuccess?.Invoke(r), onError); /// /// Adds a room to a local "server-side" list that's returned when a is fired. From d79602a9127ffcff60597dcbb8291da70566ffe2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Dec 2021 16:58:16 +0900 Subject: [PATCH 393/419] Add wait step to `TestScenePlaylistResultScreen` explicitly for screen load --- .../Visual/Playlists/TestScenePlaylistsResultsScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 25ca1299ef..e9210496ca 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -168,12 +168,13 @@ namespace osu.Game.Tests.Visual.Playlists })); }); + AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded); waitForDisplay(); } private void waitForDisplay() { - AddUntilStep("wait for load to complete", () => + AddUntilStep("wait for scores loaded", () => requestComplete && resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount && resultsScreen.ScorePanelList.AllPanelsVisible); From af78a3e99d9126d6bd47219b4fe6a1b6625376d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Dec 2021 17:09:08 +0900 Subject: [PATCH 394/419] Fix weird loop logic --- osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index be390742ea..01692ebc80 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -883,7 +883,7 @@ namespace osu.Game.Tests.Visual.SongSelect { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); - for (int i = 0; i < 100; i += 10) + for (int i = 0; i < 10; i++) manager.Import(TestResources.CreateTestBeatmapSetInfo(rulesets: usableRulesets)).Wait(); }); } From 5c8e317a6e7b647b40e1f92a9d0d6eb17dbafda5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Dec 2021 17:11:15 +0900 Subject: [PATCH 395/419] Chooser earlier items in song select tests to avoid potentially not having enough --- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 01692ebc80..dcc34d2eaa 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -488,8 +488,9 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select beatmap externally", () => { target = manager.GetAllUsableBeatmapSets() - .Where(b => b.Beatmaps.Any(bi => bi.RulesetID == targetRuleset)) - .ElementAt(5).Beatmaps.First(bi => bi.RulesetID == targetRuleset); + .First(b => b.Beatmaps.Any(bi => bi.RulesetID == targetRuleset)) + .Beatmaps + .First(bi => bi.RulesetID == targetRuleset); Beatmap.Value = manager.GetWorkingBeatmap(target); }); @@ -534,8 +535,10 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select beatmap externally", () => { - target = manager.GetAllUsableBeatmapSets().Where(b => b.Beatmaps.Any(bi => bi.RulesetID == 1)) - .ElementAt(5).Beatmaps.First(); + target = manager + .GetAllUsableBeatmapSets() + .First(b => b.Beatmaps.Any(bi => bi.RulesetID == 1)) + .Beatmaps.First(); Beatmap.Value = manager.GetWorkingBeatmap(target); }); From 6caa950c44aba65eb9aef5aa2895d79e6ee0ddda Mon Sep 17 00:00:00 2001 From: MBmasher Date: Mon, 20 Dec 2021 19:16:41 +1100 Subject: [PATCH 396/419] Rename `osuPrevious` to `osuLoop` --- .../Difficulty/Skills/Flashlight.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 9c539d5e4b..cb1ccf949e 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -42,27 +42,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills for (int i = 0; i < Previous.Count; i++) { - var osuPrevious = (OsuDifficultyHitObject)Previous[i]; - var osuPreviousHitObject = (OsuHitObject)(osuPrevious.BaseObject); + var osuLoop = (OsuDifficultyHitObject)Previous[i]; + var osuLoopHitObject = (OsuHitObject)(osuLoop.BaseObject); - OsuDifficultyHitObject osuLastPrevious; + OsuDifficultyHitObject osuLoopNext; if (i == 0) - osuLastPrevious = osuCurrent; + osuLoopNext = osuCurrent; else - osuLastPrevious = (OsuDifficultyHitObject)Previous[i - 1]; + osuLoopNext = (OsuDifficultyHitObject)Previous[i - 1]; - if (!(osuPrevious.BaseObject is Spinner)) + if (!(osuLoop.BaseObject is Spinner)) { - double jumpDistance = (osuHitObject.StackedPosition - osuPreviousHitObject.EndPosition).Length; + double jumpDistance = (osuHitObject.StackedPosition - osuLoopHitObject.EndPosition).Length; - cumulativeStrainTime += osuLastPrevious.StrainTime; + cumulativeStrainTime += osuLoopNext.StrainTime; // We want to nerf objects that can be easily seen within the Flashlight circle radius. if (i == 0) smallDistNerf = Math.Min(1.0, jumpDistance / 75.0); // We also want to nerf stacks so that only the first object of the stack is accounted for. - double stackNerf = Math.Min(1.0, (osuPrevious.JumpDistance / scalingFactor) / 25.0); + double stackNerf = Math.Min(1.0, (osuLoop.JumpDistance / scalingFactor) / 25.0); result += Math.Pow(0.8, i) * stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime; } From 090c3e84e7004e544f7c57374865c87ceac41109 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Dec 2021 17:38:14 +0900 Subject: [PATCH 397/419] Avoid blocking windows key usage when the osu! window is not active As discussed in https://github.com/ppy/osu/discussions/16147. --- osu.Desktop/Windows/GameplayWinKeyBlocker.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs index dbfd170ea1..4acaf61cea 100644 --- a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs +++ b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs @@ -14,6 +14,7 @@ namespace osu.Desktop.Windows { private Bindable disableWinKey; private IBindable localUserPlaying; + private IBindable isActive; [Resolved] private GameHost host { get; set; } @@ -24,13 +25,16 @@ namespace osu.Desktop.Windows localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy(); localUserPlaying.BindValueChanged(_ => updateBlocking()); + isActive = host.IsActive.GetBoundCopy(); + isActive.BindValueChanged(_ => updateBlocking()); + disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey); disableWinKey.BindValueChanged(_ => updateBlocking(), true); } private void updateBlocking() { - bool shouldDisable = disableWinKey.Value && localUserPlaying.Value; + bool shouldDisable = isActive.Value && disableWinKey.Value && localUserPlaying.Value; if (shouldDisable) host.InputThread.Scheduler.Add(WindowsKey.Disable); From 87051d5d61c71c8045c668686fe5b3020cb548a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Dec 2021 17:46:20 +0900 Subject: [PATCH 398/419] Add better defined steps to `TestLoseHostWhileReady` ready button test Not 100% sure this will solve the issue but it's worth a try. The button state checks are using `Until` everywhere else so this brings the test in line with the standards. As seen https://github.com/ppy/osu/runs/4579641456?check_suite_focus=true. --- .../Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 84b63a5733..81220e2527 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -163,10 +163,13 @@ namespace osu.Game.Tests.Visual.Multiplayer }); addClickButtonStep(); + AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); + AddStep("transfer host", () => Client.TransferHost(Client.Room?.Users[1].UserID ?? 0)); addClickButtonStep(); - AddAssert("match not started", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); + AddUntilStep("user is idle (match not started)", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); + AddAssert("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value); } [TestCase(true)] From 1533aefce5015d03083501d5bb6547bb77e7b5c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Dec 2021 18:22:19 +0900 Subject: [PATCH 399/419] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 209b8cd63e..1532d4ce23 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 97bdf00f69..6e6002bc8e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index d4b7339900..de359245d1 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From 6907a9a3ccb5ef46863b5eb4bd6231f678fc606c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Dec 2021 18:24:14 +0900 Subject: [PATCH 400/419] Name some screen stacks for better logging context --- osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs | 6 +++++- .../Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs | 5 ++++- osu.Game/Tests/Visual/ScreenTestScene.cs | 6 +++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs b/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs index 370f3bd0ae..88502b7074 100644 --- a/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs +++ b/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs @@ -49,7 +49,11 @@ namespace osu.Game.Tests.Visual InternalChildren = new Drawable[] { Client = new TestMultiplayerClient(RoomManager), - screenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both } + screenStack = new OsuScreenStack + { + Name = nameof(TestMultiplayerScreenStack), + RelativeSizeAxes = Axes.Both + } }; screenStack.Push(multiplayerScreen); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index c3190cd845..48f153ecbe 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -87,7 +87,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate gameplayContent.Child = new PlayerIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.BeatmapInfo), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) { RelativeSizeAxes = Axes.Both, - Child = stack = new OsuScreenStack() + Child = stack = new OsuScreenStack + { + Name = nameof(PlayerArea), + } }; stack.Push(new MultiSpectatorPlayerLoader(Score, () => new MultiSpectatorPlayer(Score, GameplayClock))); diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index aa46b516bf..c44a848275 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -29,7 +29,11 @@ namespace osu.Game.Tests.Visual { base.Content.AddRange(new Drawable[] { - Stack = new OsuScreenStack { RelativeSizeAxes = Axes.Both }, + Stack = new OsuScreenStack + { + Name = nameof(ScreenTestScene), + RelativeSizeAxes = Axes.Both + }, content = new Container { RelativeSizeAxes = Axes.Both }, DialogOverlay = new DialogOverlay() }); From ec0a6735eb519272f85413c0017e9dcf95ed11e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Dec 2021 18:24:59 +0900 Subject: [PATCH 401/419] Rename `TestMultiplayerScreenStack` to `TestMultiplayerComponents` --- .../Visual/Multiplayer/QueueModeTestScene.cs | 20 +++---- .../Multiplayer/TestSceneMultiplayer.cs | 52 +++++++++---------- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 18 +++---- .../Navigation/TestSceneScreenNavigation.cs | 8 +-- ...nStack.cs => TestMultiplayerComponents.cs} | 6 +-- 5 files changed, 52 insertions(+), 52 deletions(-) rename osu.Game.Tests/Visual/{TestMultiplayerScreenStack.cs => TestMultiplayerComponents.cs} (95%) diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 83292c36bf..5acb44ac45 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -31,16 +31,16 @@ namespace osu.Game.Tests.Visual.Multiplayer protected BeatmapInfo InitialBeatmap { get; private set; } protected BeatmapInfo OtherBeatmap { get; private set; } - protected IScreen CurrentScreen => multiplayerScreenStack.CurrentScreen; - protected IScreen CurrentSubScreen => multiplayerScreenStack.MultiplayerScreen.CurrentSubScreen; + protected IScreen CurrentScreen => multiplayerComponents.CurrentScreen; + protected IScreen CurrentSubScreen => multiplayerComponents.MultiplayerScreen.CurrentSubScreen; private BeatmapManager beatmaps; private RulesetStore rulesets; private BeatmapSetInfo importedSet; - private TestMultiplayerScreenStack multiplayerScreenStack; + private TestMultiplayerComponents multiplayerComponents; - protected TestMultiplayerClient Client => multiplayerScreenStack.Client; + protected TestMultiplayerClient Client => multiplayerComponents.Client; [Cached(typeof(UserLookupCache))] private UserLookupCache lookupCache = new TestUserLookupCache(); @@ -64,12 +64,12 @@ namespace osu.Game.Tests.Visual.Multiplayer OtherBeatmap = importedSet.Beatmaps.Last(b => b.RulesetID == 0); }); - AddStep("load multiplayer", () => LoadScreen(multiplayerScreenStack = new TestMultiplayerScreenStack())); - AddUntilStep("wait for multiplayer to load", () => multiplayerScreenStack.IsLoaded); + AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents())); + AddUntilStep("wait for multiplayer to load", () => multiplayerComponents.IsLoaded); AddUntilStep("wait for lounge to load", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - AddUntilStep("wait for lounge", () => multiplayerScreenStack.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); - AddStep("open room", () => multiplayerScreenStack.ChildrenOfType().Single().Open(new Room + AddUntilStep("wait for lounge", () => multiplayerComponents.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); + AddStep("open room", () => multiplayerComponents.ChildrenOfType().Single().Open(new Room { Name = { Value = "Test Room" }, QueueMode = { Value = Mode }, @@ -109,8 +109,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready); clickReadyButton(); - AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player player && player.IsLoaded); - AddStep("exit player", () => multiplayerScreenStack.MultiplayerScreen.MakeCurrent()); + AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player player && player.IsLoaded); + AddStep("exit player", () => multiplayerComponents.MultiplayerScreen.MakeCurrent()); } private void clickReadyButton() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 99411254a7..bd0e5c4eb9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -46,10 +46,10 @@ namespace osu.Game.Tests.Visual.Multiplayer private RulesetStore rulesets; private BeatmapSetInfo importedSet; - private TestMultiplayerScreenStack multiplayerScreenStack; + private TestMultiplayerComponents multiplayerComponents; - private TestMultiplayerClient client => multiplayerScreenStack.Client; - private TestMultiplayerRoomManager roomManager => multiplayerScreenStack.RoomManager; + private TestMultiplayerClient client => multiplayerComponents.Client; + private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager; [Cached(typeof(UserLookupCache))] private UserLookupCache lookupCache = new TestUserLookupCache(); @@ -71,8 +71,8 @@ namespace osu.Game.Tests.Visual.Multiplayer importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); }); - AddStep("load multiplayer", () => LoadScreen(multiplayerScreenStack = new TestMultiplayerScreenStack())); - AddUntilStep("wait for multiplayer to load", () => multiplayerScreenStack.IsLoaded); + AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents())); + AddUntilStep("wait for multiplayer to load", () => multiplayerComponents.IsLoaded); AddUntilStep("wait for lounge to load", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); } @@ -419,7 +419,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("Enter song select", () => { - var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerScreenStack.CurrentScreen).CurrentSubScreen; + var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen; ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.Room?.Settings.PlaylistItemId); }); @@ -433,7 +433,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("start match externally", () => client.StartMatch()); - AddUntilStep("play started", () => multiplayerScreenStack.CurrentScreen is Player); + AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID); } @@ -473,7 +473,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("start match externally", () => client.StartMatch()); - AddAssert("play not started", () => multiplayerScreenStack.IsCurrentScreen()); + AddAssert("play not started", () => multiplayerComponents.IsCurrentScreen()); } [Test] @@ -517,7 +517,7 @@ namespace osu.Game.Tests.Visual.Multiplayer importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); }); - AddUntilStep("play started", () => multiplayerScreenStack.CurrentScreen is SpectatorScreen); + AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is SpectatorScreen); } [Test] @@ -559,16 +559,16 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("open mod overlay", () => this.ChildrenOfType().Single().TriggerClick()); - AddStep("invoke on back button", () => multiplayerScreenStack.OnBackButton()); + AddStep("invoke on back button", () => multiplayerComponents.OnBackButton()); AddAssert("mod overlay is hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden); AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden); - testLeave("back button", () => multiplayerScreenStack.OnBackButton()); + testLeave("back button", () => multiplayerComponents.OnBackButton()); // mimics home button and OS window close - testLeave("forced exit", () => multiplayerScreenStack.Exit()); + testLeave("forced exit", () => multiplayerComponents.Exit()); void testLeave(string actionName, Action action) { @@ -605,7 +605,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep($"wait for time > {i}", () => this.ChildrenOfType().SingleOrDefault()?.GameplayClock.CurrentTime > time); } - AddUntilStep("wait for results", () => multiplayerScreenStack.CurrentScreen is ResultsScreen); + AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen); } [Test] @@ -680,15 +680,15 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready)); pressReadyButton(1234); - AddUntilStep("wait for gameplay", () => (multiplayerScreenStack.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true); + AddUntilStep("wait for gameplay", () => (multiplayerComponents.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true); AddStep("press back button and exit", () => { - multiplayerScreenStack.OnBackButton(); - multiplayerScreenStack.Exit(); + multiplayerComponents.OnBackButton(); + multiplayerComponents.Exit(); }); - AddUntilStep("wait for return to match subscreen", () => multiplayerScreenStack.MultiplayerScreen.IsCurrentScreen()); + AddUntilStep("wait for return to match subscreen", () => multiplayerComponents.MultiplayerScreen.IsCurrentScreen()); AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle); } @@ -716,17 +716,17 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready)); pressReadyButton(1234); - AddUntilStep("wait for gameplay", () => (multiplayerScreenStack.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true); + AddUntilStep("wait for gameplay", () => (multiplayerComponents.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true); AddStep("set other user loaded", () => client.ChangeUserState(1234, MultiplayerUserState.Loaded)); AddStep("set other user finished play", () => client.ChangeUserState(1234, MultiplayerUserState.FinishedPlay)); AddStep("press back button and exit", () => { - multiplayerScreenStack.OnBackButton(); - multiplayerScreenStack.Exit(); + multiplayerComponents.OnBackButton(); + multiplayerComponents.Exit(); }); - AddUntilStep("wait for return to match subscreen", () => multiplayerScreenStack.MultiplayerScreen.IsCurrentScreen()); + AddUntilStep("wait for return to match subscreen", () => multiplayerComponents.MultiplayerScreen.IsCurrentScreen()); AddWaitStep("wait for possible state change", 5); AddUntilStep("user state is spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating); } @@ -758,7 +758,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); - AddStep("exit gameplay as initial user", () => multiplayerScreenStack.MultiplayerScreen.MakeCurrent()); + AddStep("exit gameplay as initial user", () => multiplayerComponents.MultiplayerScreen.MakeCurrent()); AddUntilStep("queue contains item", () => this.ChildrenOfType().Single().Items.Single().ID == 2); } @@ -792,7 +792,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("delete item as other user", () => client.RemoveUserPlaylistItem(1234, 2)); AddUntilStep("item removed from playlist", () => client.Room?.Playlist.Count == 1); - AddStep("exit gameplay as initial user", () => multiplayerScreenStack.MultiplayerScreen.MakeCurrent()); + AddStep("exit gameplay as initial user", () => multiplayerComponents.MultiplayerScreen.MakeCurrent()); AddUntilStep("queue is empty", () => this.ChildrenOfType().Single().Items.Count == 0); } @@ -800,7 +800,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { pressReadyButton(); pressReadyButton(); - AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player); + AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player); } private ReadyButton readyButton => this.ChildrenOfType().Single(); @@ -826,8 +826,8 @@ namespace osu.Game.Tests.Visual.Multiplayer private void createRoom(Func room) { - AddUntilStep("wait for lounge", () => multiplayerScreenStack.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); - AddStep("open room", () => multiplayerScreenStack.ChildrenOfType().Single().Open(room())); + AddUntilStep("wait for lounge", () => multiplayerComponents.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); + AddStep("open room", () => multiplayerComponents.ChildrenOfType().Single().Open(room())); AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); AddWaitStep("wait for transition", 2); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 4ecbc30d76..81c59b90f5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -31,9 +31,9 @@ namespace osu.Game.Tests.Visual.Multiplayer private RulesetStore rulesets; private BeatmapSetInfo importedSet; - private TestMultiplayerScreenStack multiplayerScreenStack; + private TestMultiplayerComponents multiplayerComponents; - private TestMultiplayerClient client => multiplayerScreenStack.Client; + private TestMultiplayerClient client => multiplayerComponents.Client; [Cached(typeof(UserLookupCache))] private UserLookupCache lookupCache = new TestUserLookupCache(); @@ -55,8 +55,8 @@ namespace osu.Game.Tests.Visual.Multiplayer importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); }); - AddStep("load multiplayer", () => LoadScreen(multiplayerScreenStack = new TestMultiplayerScreenStack())); - AddUntilStep("wait for multiplayer to load", () => multiplayerScreenStack.IsLoaded); + AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents())); + AddUntilStep("wait for multiplayer to load", () => multiplayerComponents.IsLoaded); AddUntilStep("wait for lounge to load", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); } @@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("press own button", () => { - InputManager.MoveMouseTo(multiplayerScreenStack.ChildrenOfType().First()); + InputManager.MoveMouseTo(multiplayerComponents.ChildrenOfType().First()); InputManager.Click(MouseButton.Left); }); AddAssert("user on team 1", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1); @@ -113,7 +113,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("press other user's button", () => { - InputManager.MoveMouseTo(multiplayerScreenStack.ChildrenOfType().ElementAt(1)); + InputManager.MoveMouseTo(multiplayerComponents.ChildrenOfType().ElementAt(1)); InputManager.Click(MouseButton.Left); }); AddAssert("user still on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); @@ -164,18 +164,18 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead); - AddUntilStep("team displays are not displaying teams", () => multiplayerScreenStack.ChildrenOfType().All(d => d.DisplayedTeam == null)); + AddUntilStep("team displays are not displaying teams", () => multiplayerComponents.ChildrenOfType().All(d => d.DisplayedTeam == null)); AddStep("change to team vs", () => client.ChangeSettings(matchType: MatchType.TeamVersus)); AddUntilStep("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus); - AddUntilStep("team displays are displaying teams", () => multiplayerScreenStack.ChildrenOfType().All(d => d.DisplayedTeam != null)); + AddUntilStep("team displays are displaying teams", () => multiplayerComponents.ChildrenOfType().All(d => d.DisplayedTeam != null)); } private void createRoom(Func room) { - AddStep("open room", () => multiplayerScreenStack.ChildrenOfType().Single().Open(room())); + AddStep("open room", () => multiplayerComponents.ChildrenOfType().Single().Open(room())); AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); AddWaitStep("wait for transition", 2); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 664c186cf8..48ab643992 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -336,12 +336,12 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestPushMatchSubScreenAndPressBackButtonImmediately() { - TestMultiplayerScreenStack multiplayerScreenStack = null; + TestMultiplayerComponents multiplayerComponents = null; - PushAndConfirm(() => multiplayerScreenStack = new TestMultiplayerScreenStack()); + PushAndConfirm(() => multiplayerComponents = new TestMultiplayerComponents()); - AddUntilStep("wait for lounge", () => multiplayerScreenStack.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); - AddStep("open room", () => multiplayerScreenStack.ChildrenOfType().Single().Open()); + AddUntilStep("wait for lounge", () => multiplayerComponents.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); + AddStep("open room", () => multiplayerComponents.ChildrenOfType().Single().Open()); AddStep("press back button", () => Game.ChildrenOfType().First().Action()); AddWaitStep("wait two frames", 2); } diff --git a/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs similarity index 95% rename from osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs rename to osu.Game.Tests/Visual/TestMultiplayerComponents.cs index 88502b7074..cd7a936778 100644 --- a/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs +++ b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual /// ///

///
- public class TestMultiplayerScreenStack : OsuScreen + public class TestMultiplayerComponents : OsuScreen { public Screens.OnlinePlay.Multiplayer.Multiplayer MultiplayerScreen => multiplayerScreen; @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual private readonly OsuScreenStack screenStack; private readonly TestMultiplayer multiplayerScreen; - public TestMultiplayerScreenStack() + public TestMultiplayerComponents() { multiplayerScreen = new TestMultiplayer(); @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual Client = new TestMultiplayerClient(RoomManager), screenStack = new OsuScreenStack { - Name = nameof(TestMultiplayerScreenStack), + Name = nameof(TestMultiplayerComponents), RelativeSizeAxes = Axes.Both } }; From f492cf84d9d83d8ea9a39e5c1c511471d5bd7f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Dec 2021 11:24:40 +0100 Subject: [PATCH 402/419] Ensure presence of at least 1 difficulty for each ruleset --- .../SongSelect/TestScenePlaySongSelect.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index dcc34d2eaa..912d3f838c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -466,7 +466,9 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestExternalBeatmapChangeWhileFiltered(bool differentRuleset) { createSongSelect(); - addManyTestMaps(); + // ensure there is at least 1 difficulty for each of the rulesets + // (catch is excluded inside of addManyTestMaps). + addManyTestMaps(3); changeRuleset(0); @@ -519,7 +521,9 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestExternalBeatmapChangeWhileFilteredThenRefilter() { createSongSelect(); - addManyTestMaps(); + // ensure there is at least 1 difficulty for each of the rulesets + // (catch is excluded inside of addManyTestMaps). + addManyTestMaps(3); changeRuleset(0); @@ -880,14 +884,21 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("wait for carousel loaded", () => songSelect.Carousel.IsAlive); } - private void addManyTestMaps() + /// + /// Imports test beatmap sets to show in the carousel. + /// + /// + /// The exact count of difficulties to create for each beatmap set. + /// A value causes the count of difficulties to be selected randomly. + /// + private void addManyTestMaps(int? difficultyCountPerSet = null) { AddStep("import test maps", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); for (int i = 0; i < 10; i++) - manager.Import(TestResources.CreateTestBeatmapSetInfo(rulesets: usableRulesets)).Wait(); + manager.Import(TestResources.CreateTestBeatmapSetInfo(difficultyCountPerSet, usableRulesets)).Wait(); }); } From a59583ee0901c3086059cd38b44ffcb86c7729b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Dec 2021 13:18:02 +0100 Subject: [PATCH 403/419] Add extension method for returning next playlist item --- .../OnlinePlay/PlaylistExtensionsTest.cs | 72 +++++++++++++++++++ osu.Game/Online/Rooms/PlaylistExtensions.cs | 10 +++ 2 files changed, 82 insertions(+) create mode 100644 osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs diff --git a/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs b/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs new file mode 100644 index 0000000000..d2ee47bc23 --- /dev/null +++ b/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs @@ -0,0 +1,72 @@ +// 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.Online.Rooms; + +namespace osu.Game.Tests.OnlinePlay +{ + [TestFixture] + public class PlaylistExtensionsTest + { + [Test] + public void TestPlaylistItemsInOrder() + { + var items = new[] + { + new PlaylistItem { ID = 1, BeatmapID = 1001, PlaylistOrder = 1 }, + new PlaylistItem { ID = 2, BeatmapID = 1002, PlaylistOrder = 2 }, + new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 }, + }; + + var nextItem = items.GetNextItem(); + + Assert.That(nextItem, Is.EqualTo(items[0])); + } + + [Test] + public void TestPlaylistItemsOutOfOrder() + { + var items = new[] + { + new PlaylistItem { ID = 2, BeatmapID = 1002, PlaylistOrder = 2 }, + new PlaylistItem { ID = 1, BeatmapID = 1001, PlaylistOrder = 1 }, + new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 }, + }; + + var nextItem = items.GetNextItem(); + + Assert.That(nextItem, Is.EqualTo(items[1])); + } + + [Test] + public void TestExpiredPlaylistItemsSkipped() + { + var items = new[] + { + new PlaylistItem { ID = 2, BeatmapID = 1002, PlaylistOrder = 2, Expired = true }, + new PlaylistItem { ID = 1, BeatmapID = 1001, PlaylistOrder = 1, Expired = true }, + new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 }, + }; + + var nextItem = items.GetNextItem(); + + Assert.That(nextItem, Is.EqualTo(items[2])); + } + + [Test] + public void TestAllItemsExpired() + { + var items = new[] + { + new PlaylistItem { ID = 2, BeatmapID = 1002, PlaylistOrder = 2, Expired = true }, + new PlaylistItem { ID = 1, BeatmapID = 1001, PlaylistOrder = 1, Expired = true }, + new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3, Expired = true }, + }; + + var nextItem = items.GetNextItem(); + + Assert.That(nextItem, Is.Null); + } + } +} diff --git a/osu.Game/Online/Rooms/PlaylistExtensions.cs b/osu.Game/Online/Rooms/PlaylistExtensions.cs index 992011da3c..e900dc53ef 100644 --- a/osu.Game/Online/Rooms/PlaylistExtensions.cs +++ b/osu.Game/Online/Rooms/PlaylistExtensions.cs @@ -1,6 +1,9 @@ // 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.Collections.Generic; using System.Linq; using Humanizer; using Humanizer.Localisation; @@ -10,6 +13,13 @@ namespace osu.Game.Online.Rooms { public static class PlaylistExtensions { + /// + /// Returns the next to be played from the supplied , + /// or if all items are expired. + /// + public static PlaylistItem? GetNextItem(this IEnumerable playlist) => + playlist.OrderBy(item => item.PlaylistOrder).FirstOrDefault(item => !item.Expired); + public static string GetTotalDuration(this BindableList playlist) => playlist.Select(p => p.Beatmap.Value.Length).Sum().Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2); } From a5a9922f81529f46d2efe46e11af0c6822512bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Dec 2021 13:19:00 +0100 Subject: [PATCH 404/419] Fix lounge screen content not matching current room playlist item --- osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs | 8 ++++---- osu.Game/Online/Rooms/PlaylistExtensions.cs | 4 ++-- .../OnlinePlay/Components/OnlinePlayBackgroundSprite.cs | 4 ++-- .../Screens/OnlinePlay/Lounge/LoungeBackgroundScreen.cs | 3 +-- osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs | 5 ++--- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs b/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs index d2ee47bc23..001513be1f 100644 --- a/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs +++ b/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.OnlinePlay new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 }, }; - var nextItem = items.GetNextItem(); + var nextItem = items.GetCurrentItem(); Assert.That(nextItem, Is.EqualTo(items[0])); } @@ -34,7 +34,7 @@ namespace osu.Game.Tests.OnlinePlay new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 }, }; - var nextItem = items.GetNextItem(); + var nextItem = items.GetCurrentItem(); Assert.That(nextItem, Is.EqualTo(items[1])); } @@ -49,7 +49,7 @@ namespace osu.Game.Tests.OnlinePlay new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 }, }; - var nextItem = items.GetNextItem(); + var nextItem = items.GetCurrentItem(); Assert.That(nextItem, Is.EqualTo(items[2])); } @@ -64,7 +64,7 @@ namespace osu.Game.Tests.OnlinePlay new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3, Expired = true }, }; - var nextItem = items.GetNextItem(); + var nextItem = items.GetCurrentItem(); Assert.That(nextItem, Is.Null); } diff --git a/osu.Game/Online/Rooms/PlaylistExtensions.cs b/osu.Game/Online/Rooms/PlaylistExtensions.cs index e900dc53ef..46cb218910 100644 --- a/osu.Game/Online/Rooms/PlaylistExtensions.cs +++ b/osu.Game/Online/Rooms/PlaylistExtensions.cs @@ -14,10 +14,10 @@ namespace osu.Game.Online.Rooms public static class PlaylistExtensions { /// - /// Returns the next to be played from the supplied , + /// Returns the first non-expired in playlist order from the supplied , /// or if all items are expired. /// - public static PlaylistItem? GetNextItem(this IEnumerable playlist) => + public static PlaylistItem? GetCurrentItem(this IEnumerable playlist) => playlist.OrderBy(item => item.PlaylistOrder).FirstOrDefault(item => !item.Expired); public static string GetTotalDuration(this BindableList playlist) => diff --git a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs index e2ba0b03b0..d144e1e3a9 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs @@ -1,10 +1,10 @@ // 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.Game.Beatmaps.Drawables; +using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components { @@ -30,7 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Components private void updateBeatmap() { - sprite.Beatmap.Value = Playlist.FirstOrDefault()?.Beatmap.Value; + sprite.Beatmap.Value = Playlist.GetCurrentItem()?.Beatmap.Value; } protected virtual UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new UpdateableBeatmapBackgroundSprite(BeatmapSetCoverType) { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeBackgroundScreen.cs index 6c00ca2e81..926c35c5da 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeBackgroundScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeBackgroundScreen.cs @@ -3,7 +3,6 @@ #nullable enable -using System.Linq; using osu.Framework.Bindables; using osu.Framework.Screens; using osu.Game.Online.Rooms; @@ -20,7 +19,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge public LoungeBackgroundScreen() { SelectedRoom.BindValueChanged(onSelectedRoomChanged); - playlist.BindCollectionChanged((_, __) => PlaylistItem = playlist.FirstOrDefault()); + playlist.BindCollectionChanged((_, __) => PlaylistItem = playlist.GetCurrentItem()); } private void onSelectedRoomChanged(ValueChangedEvent room) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index 6b111d76a5..c833621fbc 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; @@ -73,7 +72,7 @@ namespace osu.Game.Screens.OnlinePlay private IBindable subScreenSelectedItem { get; set; } /// - /// The currently selected item in the , or the last item from + /// The currently selected item in the , or the current item from /// if this is not within a . /// protected readonly Bindable SelectedItem = new Bindable(); @@ -88,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay protected virtual void UpdateSelectedItem() => SelectedItem.Value = RoomID.Value == null || subScreenSelectedItem == null - ? Playlist.LastOrDefault() + ? Playlist.GetCurrentItem() : subScreenSelectedItem.Value; } } From 0975f570ba2bdf82dd4aee1bd36412b94cb0b31e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Dec 2021 13:42:49 +0100 Subject: [PATCH 405/419] Return last playlist item if all expired --- .../OnlinePlay/PlaylistExtensionsTest.cs | 29 ++++++++++++++----- osu.Game/Online/Rooms/PlaylistExtensions.cs | 14 +++++++-- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs b/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs index 001513be1f..5913cb0f29 100644 --- a/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs +++ b/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using NUnit.Framework; using osu.Game.Online.Rooms; @@ -9,6 +10,17 @@ namespace osu.Game.Tests.OnlinePlay [TestFixture] public class PlaylistExtensionsTest { + [Test] + public void TestEmpty() + { + // mostly an extreme edge case, i.e. during room creation. + var items = Array.Empty(); + + var currentItem = items.GetCurrentItem(); + + Assert.That(currentItem, Is.Null); + } + [Test] public void TestPlaylistItemsInOrder() { @@ -19,9 +31,9 @@ namespace osu.Game.Tests.OnlinePlay new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 }, }; - var nextItem = items.GetCurrentItem(); + var currentItem = items.GetCurrentItem(); - Assert.That(nextItem, Is.EqualTo(items[0])); + Assert.That(currentItem, Is.EqualTo(items[0])); } [Test] @@ -34,9 +46,9 @@ namespace osu.Game.Tests.OnlinePlay new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 }, }; - var nextItem = items.GetCurrentItem(); + var currentItem = items.GetCurrentItem(); - Assert.That(nextItem, Is.EqualTo(items[1])); + Assert.That(currentItem, Is.EqualTo(items[1])); } [Test] @@ -49,9 +61,9 @@ namespace osu.Game.Tests.OnlinePlay new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 }, }; - var nextItem = items.GetCurrentItem(); + var currentItem = items.GetCurrentItem(); - Assert.That(nextItem, Is.EqualTo(items[2])); + Assert.That(currentItem, Is.EqualTo(items[2])); } [Test] @@ -64,9 +76,10 @@ namespace osu.Game.Tests.OnlinePlay new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3, Expired = true }, }; - var nextItem = items.GetCurrentItem(); + var currentItem = items.GetCurrentItem(); - Assert.That(nextItem, Is.Null); + // if all items are expired, the last-played item is expected to be returned. + Assert.That(currentItem, Is.EqualTo(items[2])); } } } diff --git a/osu.Game/Online/Rooms/PlaylistExtensions.cs b/osu.Game/Online/Rooms/PlaylistExtensions.cs index 46cb218910..355ae6188d 100644 --- a/osu.Game/Online/Rooms/PlaylistExtensions.cs +++ b/osu.Game/Online/Rooms/PlaylistExtensions.cs @@ -15,10 +15,18 @@ namespace osu.Game.Online.Rooms { /// /// Returns the first non-expired in playlist order from the supplied , - /// or if all items are expired. + /// or the last-played if all items are expired, + /// or if was empty. /// - public static PlaylistItem? GetCurrentItem(this IEnumerable playlist) => - playlist.OrderBy(item => item.PlaylistOrder).FirstOrDefault(item => !item.Expired); + public static PlaylistItem? GetCurrentItem(this ICollection playlist) + { + if (playlist.Count == 0) + return null; + + return playlist.All(item => item.Expired) + ? playlist.OrderByDescending(item => item.PlaylistOrder).First() + : playlist.OrderBy(item => item.PlaylistOrder).First(item => !item.Expired); + } public static string GetTotalDuration(this BindableList playlist) => playlist.Select(p => p.Beatmap.Value.Length).Sum().Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2); From e21dbf10ff7734be7b0493d488f311e1a1657f1b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 Dec 2021 12:25:32 +0900 Subject: [PATCH 406/419] Refactor further to remove indexing confusion --- .../Difficulty/Skills/Flashlight.cs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 21a2fc2252..ad7464244e 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -40,32 +40,33 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills double result = 0.0; + OsuDifficultyHitObject lastObj = null; + + // This is iterating backwards in time from the current object. for (int i = 0; i < Previous.Count; i++) { - var osuLoop = (OsuDifficultyHitObject)Previous[i]; - var osuLoopHitObject = (OsuHitObject)(osuLoop.BaseObject); + var currentObj = (OsuDifficultyHitObject)Previous[i]; + var currentHitObject = (OsuHitObject)(currentObj.BaseObject); - OsuDifficultyHitObject osuLoopNext; - if (i == 0) - osuLoopNext = osuCurrent; - else - osuLoopNext = (OsuDifficultyHitObject)Previous[i - 1]; + lastObj ??= currentObj; - if (!(osuLoop.BaseObject is Spinner)) + if (!(currentObj.BaseObject is Spinner)) { - double jumpDistance = (osuHitObject.StackedPosition - osuLoopHitObject.EndPosition).Length; + double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.EndPosition).Length; - cumulativeStrainTime += osuLoopNext.StrainTime; + cumulativeStrainTime += lastObj.StrainTime; // We want to nerf objects that can be easily seen within the Flashlight circle radius. if (i == 0) smallDistNerf = Math.Min(1.0, jumpDistance / 75.0); // We also want to nerf stacks so that only the first object of the stack is accounted for. - double stackNerf = Math.Min(1.0, (osuLoop.LazyJumpDistance / scalingFactor) / 25.0); + double stackNerf = Math.Min(1.0, (currentObj.LazyJumpDistance / scalingFactor) / 25.0); result += Math.Pow(0.8, i) * stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime; } + + lastObj = currentObj; } return Math.Pow(smallDistNerf * result, 2.0); From 28d6ff5d9c8bac9390e7576829623798934fc61a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Dec 2021 12:36:01 +0900 Subject: [PATCH 407/419] Fix potential wrong thread mutation in `ColourHitErrorMeter` --- .../Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs index 5012be7249..00e6059541 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs @@ -53,13 +53,13 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters LayoutEasing = Easing.OutQuint; } - public void Push(Color4 colour) + public void Push(Color4 colour) => Schedule(() => { Add(new HitErrorCircle(colour, drawable_judgement_size)); if (Children.Count > max_available_judgements) Children.FirstOrDefault(c => !c.IsRemoved)?.Remove(); - } + }); } internal class HitErrorCircle : Container From c21b2d166223d2305a0f3d7ce8d9fc77c3bc4a66 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 Dec 2021 12:39:07 +0900 Subject: [PATCH 408/419] Fix incorrect variable --- osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index ad7464244e..bdf7032521 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills var currentObj = (OsuDifficultyHitObject)Previous[i]; var currentHitObject = (OsuHitObject)(currentObj.BaseObject); - lastObj ??= currentObj; + lastObj ??= osuCurrent; if (!(currentObj.BaseObject is Spinner)) { From f366cdc73eabbaff9224e3c7010e3f641a079158 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 Dec 2021 12:39:34 +0900 Subject: [PATCH 409/419] Extract initial set out of loop --- osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index bdf7032521..ff0780be85 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills double result = 0.0; - OsuDifficultyHitObject lastObj = null; + OsuDifficultyHitObject lastObj = osuCurrent; // This is iterating backwards in time from the current object. for (int i = 0; i < Previous.Count; i++) @@ -48,8 +48,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills var currentObj = (OsuDifficultyHitObject)Previous[i]; var currentHitObject = (OsuHitObject)(currentObj.BaseObject); - lastObj ??= osuCurrent; - if (!(currentObj.BaseObject is Spinner)) { double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.EndPosition).Length; From 52db7b36fc44a66d64a5b68d5a266ece48a2cebc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Dec 2021 12:55:21 +0900 Subject: [PATCH 410/419] Move `Schedule` call to base implementation of error meter for extra safety --- .../Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs | 4 ++-- osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs index 00e6059541..5012be7249 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs @@ -53,13 +53,13 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters LayoutEasing = Easing.OutQuint; } - public void Push(Color4 colour) => Schedule(() => + public void Push(Color4 colour) { Add(new HitErrorCircle(colour, drawable_judgement_size)); if (Children.Count > max_available_judgements) Children.FirstOrDefault(c => !c.IsRemoved)?.Remove(); - }); + } } internal class HitErrorCircle : Container diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs index c7b06a3a2c..16c23f07f5 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs @@ -40,9 +40,14 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters if (gameplayClockContainer != null) gameplayClockContainer.OnSeek += Clear; - processor.NewJudgement += OnNewJudgement; + // Scheduled as meter implementations are likely going to change/add drawables when reacting to this. + processor.NewJudgement += j => Schedule(() => OnNewJudgement(j)); } + /// + /// Fired when a new judgement arrives. + /// + /// The new judgement. protected abstract void OnNewJudgement(JudgementResult judgement); protected Color4 GetColourForHitResult(HitResult result) From edcbd4de6d27cd9788523064207e4111d2d99f80 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Dec 2021 13:05:26 +0900 Subject: [PATCH 411/419] Fix incorrect event unbind logic --- osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs index 16c23f07f5..1f08cb8aa7 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs @@ -40,10 +40,12 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters if (gameplayClockContainer != null) gameplayClockContainer.OnSeek += Clear; - // Scheduled as meter implementations are likely going to change/add drawables when reacting to this. - processor.NewJudgement += j => Schedule(() => OnNewJudgement(j)); + processor.NewJudgement += processorNewJudgement; } + // Scheduled as meter implementations are likely going to change/add drawables when reacting to this. + private void processorNewJudgement(JudgementResult j) => Schedule(() => OnNewJudgement(j)); + /// /// Fired when a new judgement arrives. /// @@ -89,7 +91,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters base.Dispose(isDisposing); if (processor != null) - processor.NewJudgement -= OnNewJudgement; + processor.NewJudgement -= processorNewJudgement; if (gameplayClockContainer != null) gameplayClockContainer.OnSeek -= Clear; From 9aff646ff49ac0194b194621b9c90e1af9ab2cc9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Dec 2021 13:20:12 +0900 Subject: [PATCH 412/419] Centralise all multiplayer button clicking test logic This adds the "wait for enabled" check in a way that can be easily reused, as it keeps getting missed in test implementations. This particular commit hopefully fixes https://github.com/ppy/osu/runs/4583845033?check_suite_focus=true. --- .../Visual/Multiplayer/QueueModeTestScene.cs | 23 ++----------- .../Multiplayer/TestSceneMultiplayer.cs | 18 ++-------- .../TestSceneMultiplayerMatchSubScreen.cs | 33 ++++--------------- .../TestSceneMultiplayerReadyButton.cs | 28 +++++----------- .../TestSceneMultiplayerSpectateButton.cs | 17 +++------- .../Visual/OsuManualInputManagerTestScene.cs | 23 +++++++++++++ 6 files changed, 48 insertions(+), 94 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 5acb44ac45..88c54eb2bb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -5,7 +5,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Graphics.UserInterface; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; @@ -20,7 +19,6 @@ using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.Play; using osu.Game.Tests.Resources; -using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { @@ -86,11 +84,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); AddWaitStep("wait for transition", 2); - AddStep("create room", () => - { - InputManager.MoveMouseTo(this.ChildrenOfType().Single()); - InputManager.Click(MouseButton.Left); - }); + ClickButtonWhenEnabled(); AddUntilStep("wait for join", () => Client.RoomJoined); } @@ -104,24 +98,13 @@ namespace osu.Game.Tests.Visual.Multiplayer protected void RunGameplay() { AddUntilStep("wait for idle", () => Client.LocalUser?.State == MultiplayerUserState.Idle); - clickReadyButton(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready); - clickReadyButton(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player player && player.IsLoaded); AddStep("exit player", () => multiplayerComponents.MultiplayerScreen.MakeCurrent()); } - - private void clickReadyButton() - { - AddUntilStep("wait for ready button to be enabled", () => this.ChildrenOfType().Single().ChildrenOfType