From a393b3c6b1187f40df5a176103a3ea8baf6bd7ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 Aug 2025 09:41:56 +0200 Subject: [PATCH 01/57] Refresh realm before performing song select refetches following an online metadata lookup Probably closes https://github.com/ppy/osu/issues/34716 Can't see any other cause, can reproduce the issue on master using manual db modifications via realm studio and it is not a consistent reproduction, so seems like an open-and-shut lack of refresh. --- osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs | 1 + osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs index 5065b2d875..37ac4cdb20 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs @@ -440,6 +440,7 @@ namespace osu.Game.Screens.SelectV2 string[] tags = realm.Run(r => { // need to refetch because `beatmap.Value.BeatmapInfo` is not going to have the latest tags + r.Refresh(); var refetchedBeatmap = r.Find(beatmap.Value.BeatmapInfo.ID); return refetchedBeatmap?.Metadata.UserTags.ToArray() ?? []; }); diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs index a6917cd60f..157e2c2896 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs @@ -332,6 +332,7 @@ namespace osu.Game.Screens.SelectV2 // which prevents working beatmap refetches caused by changes to the realm model of perceived low importance). var status = realm.Run(r => { + r.Refresh(); var refetchedBeatmap = r.Find(working.Value.BeatmapInfo.ID); return refetchedBeatmap?.Status; }); From 62b4999184546e5026e8d31b7cb1baabbb44b326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Aug 2025 11:22:37 +0200 Subject: [PATCH 02/57] Add failing test case --- .../TestSceneMultiSpectatorScreen.cs | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index faf8f35a8e..b9c77e20c0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -28,6 +28,7 @@ using osu.Game.Storyboards; using osu.Game.Tests.Beatmaps.IO; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { @@ -302,9 +303,69 @@ namespace osu.Game.Tests.Visual.Multiplayer AddWaitStep("wait a bit", 10); } + [Test] + [Explicit("Test relies on timing of arriving frames to exercise assertions which doesn't work headless.")] + public void TestMaximisedUserIsAudioSource() + { + start(new[] { PLAYER_1_ID, PLAYER_2_ID }); + loadSpectateScreen(); + + // With no frames, the synchronisation state will be TooFarAhead. + // In this state, all players should be muted. + assertMuted(PLAYER_1_ID, true); + assertMuted(PLAYER_2_ID, true); + + // Send frames for both players. + sendFrames(PLAYER_1_ID, 20); + sendFrames(PLAYER_2_ID, 40); + + waitUntilRunning(PLAYER_1_ID); + AddStep("maximise player 1", () => + { + InputManager.MoveMouseTo(getInstance(PLAYER_1_ID)); + InputManager.Click(MouseButton.Left); + }); + assertMuted(PLAYER_1_ID, false); + assertMuted(PLAYER_2_ID, true); + + waitUntilPaused(PLAYER_1_ID); + assertMuted(PLAYER_1_ID, false); + assertMuted(PLAYER_2_ID, true); + + AddStep("minimise player 1", () => + { + InputManager.MoveMouseTo(getInstance(PLAYER_1_ID)); + InputManager.Click(MouseButton.Left); + }); + assertMuted(PLAYER_1_ID, true); + assertMuted(PLAYER_2_ID, false); + + AddStep("maximise player 2", () => + { + InputManager.MoveMouseTo(getInstance(PLAYER_2_ID)); + InputManager.Click(MouseButton.Left); + }); + assertMuted(PLAYER_1_ID, true); + assertMuted(PLAYER_2_ID, false); + + waitUntilPaused(PLAYER_2_ID); + sendFrames(PLAYER_1_ID, 60); + + assertMuted(PLAYER_1_ID, true); + assertMuted(PLAYER_2_ID, false); + + AddStep("minimise player 2", () => + { + InputManager.MoveMouseTo(getInstance(PLAYER_2_ID)); + InputManager.Click(MouseButton.Left); + }); + assertMuted(PLAYER_1_ID, false); + assertMuted(PLAYER_2_ID, true); + } + [Test] [FlakyTest] - public void TestMostInSyncUserIsAudioSource() + public void TestMostInSyncUserIsAudioSourceIfNoneMaximised() { start(new[] { PLAYER_1_ID, PLAYER_2_ID }); loadSpectateScreen(); From 083365f3320c96d9655d9e73c794017510410bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Aug 2025 12:03:35 +0200 Subject: [PATCH 03/57] Always use audio from maximised player if there is one in multiplayer spectator --- .../Spectate/MultiSpectatorScreen.cs | 32 +++++++++++++------ .../Multiplayer/Spectate/PlayerGrid.cs | 8 ++++- .../Multiplayer/Spectate/PlayerGrid_Cell.cs | 8 +---- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 1f96f0d371..200e6a715d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -178,17 +178,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { base.Update(); - if (!isCandidateAudioSource(currentAudioSource?.SpectatorPlayerClock)) - { - currentAudioSource = instances.Where(i => isCandidateAudioSource(i.SpectatorPlayerClock)).MinBy(i => Math.Abs(i.SpectatorPlayerClock.CurrentTime - syncManager.CurrentMasterTime)); + checkAudioSource(); + } - // Only bind adjustments if there's actually a valid source, else just use the previous ones to ensure no sudden changes to audio. - if (currentAudioSource != null) - bindAudioAdjustments(currentAudioSource); + private void checkAudioSource() + { + // always use the maximised player instance as the current audio source if there is one + if (grid.MaximisedCell?.Content is PlayerArea maximisedPlayer && maximisedPlayer == currentAudioSource) + return; - foreach (var instance in instances) - instance.Mute = instance != currentAudioSource; - } + // if there is no maximised player instance and the previous audio source is still good to use, keep using it + if (grid.MaximisedCell == null && isCandidateAudioSource(currentAudioSource?.SpectatorPlayerClock)) + return; + + // at this point we're in one of the following scenarios: + // - the maximised player instance is not the current audio source => we want to switch to the maximised player instance + // - there is no maximised player instance, and the previous audio source is stopped => find another running audio source + currentAudioSource = grid.MaximisedCell?.Content as PlayerArea + ?? instances.Where(i => isCandidateAudioSource(i.SpectatorPlayerClock)).MinBy(i => Math.Abs(i.SpectatorPlayerClock.CurrentTime - syncManager.CurrentMasterTime)); + + // Only bind adjustments if there's actually a valid source, else just use the previous ones to ensure no sudden changes to audio. + if (currentAudioSource != null) + bindAudioAdjustments(currentAudioSource); + + foreach (var instance in instances) + instance.Mute = instance != currentAudioSource; } private void bindAudioAdjustments(PlayerArea first) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs index 6e71c010e5..c3ad14dba2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs @@ -31,6 +31,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public Facade MaximisedFacade { get; } + /// + /// The currently-maximised cell. + /// + public Cell? MaximisedCell { get; private set; } + private readonly Container paddingContainer; private readonly FillFlowContainer facadeContainer; private readonly Container cellContainer; @@ -99,7 +104,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private void toggleMaximisationState(Cell target) { // in the case the target is the already maximised cell (or there is only one cell), no cell should be maximised. - bool hasMaximised = !target.IsMaximised && cellContainer.Count > 1; + bool hasMaximised = target != MaximisedCell && cellContainer.Count > 1; + MaximisedCell = hasMaximised ? target : null; // Iterate through all cells to ensure only one is maximised at any time. foreach (var cell in cellContainer.ToList()) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs index bc31299615..d1ba214117 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// A cell of the grid. Contains the content and tracks to the linked facade. /// - private partial class Cell : CompositeDrawable + public partial class Cell : CompositeDrawable { /// /// The index of the original facade of this cell. @@ -33,11 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public Action? ToggleMaximisationState; - /// - /// Whether this cell is currently maximised. - /// - public bool IsMaximised { get; private set; } - private Facade facade; private bool isAnimating; @@ -83,7 +78,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public void SetFacade(Facade newFacade, bool isMaximised) { facade = newFacade; - IsMaximised = isMaximised; isAnimating = true; TweenEdgeEffectTo(new EdgeEffectParameters From 92016a7d9b34a8394fe1723b9b0a9d3616dc81ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 20 Aug 2025 14:01:29 +0200 Subject: [PATCH 04/57] Add and use new mod icon assets --- .../Mods/CatchModFloatingFruits.cs | 3 +- .../Mods/CatchModMovingFast.cs | 3 +- .../Mods/ManiaModConstantSpeed.cs | 3 +- osu.Game.Rulesets.Mania/Mods/ManiaModCover.cs | 3 + .../Mods/ManiaModDualStages.cs | 3 + .../Mods/ManiaModFadeIn.cs | 3 + .../Mods/ManiaModHoldOff.cs | 3 +- .../Mods/ManiaModInvert.cs | 3 +- osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs | 3 + osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs | 3 + osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs | 3 + osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs | 3 + osu.Game.Rulesets.Mania/Mods/ManiaModKey4.cs | 3 + osu.Game.Rulesets.Mania/Mods/ManiaModKey5.cs | 3 + osu.Game.Rulesets.Mania/Mods/ManiaModKey6.cs | 3 + osu.Game.Rulesets.Mania/Mods/ManiaModKey7.cs | 3 + osu.Game.Rulesets.Mania/Mods/ManiaModKey8.cs | 3 + osu.Game.Rulesets.Mania/Mods/ManiaModKey9.cs | 3 + .../Mods/ManiaModNoRelease.cs | 4 + osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs | 3 +- .../Mods/OsuModApproachDifferent.cs | 3 +- osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs | 3 +- osu.Game.Rulesets.Osu/Mods/OsuModBloom.cs | 3 + osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs | 4 + osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs | 3 +- osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs | 3 +- .../Mods/OsuModFreezeFrame.cs | 4 + osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs | 3 +- .../Mods/OsuModMagnetised.cs | 3 +- osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs | 3 + osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs | 3 + osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs | 3 +- .../Mods/OsuModStrictTracking.cs | 3 + .../Mods/OsuModTargetPractice.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 3 + osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs | 3 +- osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs | 3 +- .../Mods/TaikoModConstantSpeed.cs | 3 +- .../Mods/TaikoModSimplifiedRhythm.cs | 3 + .../Mods/TaikoModSingleTap.cs | 3 + osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs | 3 + osu.Game/Graphics/OsuIcon.cs | 321 ++++++++++++++++-- .../Rulesets/Mods/ModAccuracyChallenge.cs | 4 + osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 4 + osu.Game/Rulesets/Mods/ModAutoplay.cs | 2 +- osu.Game/Rulesets/Mods/ModBarrelRoll.cs | 3 + osu.Game/Rulesets/Mods/ModClassic.cs | 3 +- osu.Game/Rulesets/Mods/ModDaycore.cs | 3 +- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 3 +- osu.Game/Rulesets/Mods/ModHalfTime.cs | 2 +- osu.Game/Rulesets/Mods/ModMirror.cs | 4 + osu.Game/Rulesets/Mods/ModMuted.cs | 3 +- osu.Game/Rulesets/Mods/ModNoMod.cs | 3 +- osu.Game/Rulesets/Mods/ModNoScope.cs | 3 +- osu.Game/Rulesets/Mods/ModRandom.cs | 2 +- osu.Game/Rulesets/Mods/ModScoreV2.cs | 3 + osu.Game/Rulesets/Mods/ModSynesthesia.cs | 3 + osu.Game/Rulesets/Mods/ModTouchDevice.cs | 2 +- osu.Game/Rulesets/Mods/ModWindDown.cs | 3 +- osu.Game/Rulesets/Mods/ModWindUp.cs | 3 +- 60 files changed, 449 insertions(+), 50 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs index f933b9a28f..88f26bfd63 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Mods public override string Acronym => "FF"; public override LocalisableString Description => "The fruits are... floating?"; public override double ScoreMultiplier => 1; - public override IconUsage? Icon => FontAwesome.Solid.Cloud; + public override IconUsage? Icon => OsuIcon.ModFloatingFruits; public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModMovingFast.cs b/osu.Game.Rulesets.Catch/Mods/CatchModMovingFast.cs index b298e5215c..4612ed62ac 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModMovingFast.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModMovingFast.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Mods; @@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Mods public override LocalisableString Description => "Dashing by default, slow down!"; public override ModType Type => ModType.Fun; public override double ScoreMultiplier => 1; - public override IconUsage? Icon => FontAwesome.Solid.Running; + public override IconUsage? Icon => OsuIcon.ModMovingFast; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) }; private DrawableCatchRuleset drawableRuleset = null!; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs index d8e6bcd424..ab493410a5 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; @@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override LocalisableString Description => "No more tricky speed changes!"; - public override IconUsage? Icon => FontAwesome.Solid.Equals; + public override IconUsage? Icon => OsuIcon.ModConstantSpeed; public override ModType Type => ModType.Conversion; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModCover.cs index b7b53587ab..3ebfcedfd1 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModCover.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModCover.cs @@ -4,8 +4,10 @@ using System; using System.Linq; using osu.Framework.Bindables; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Rulesets.Mania.UI; namespace osu.Game.Rulesets.Mania.Mods @@ -14,6 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods { public override string Name => "Cover"; public override string Acronym => "CO"; + public override IconUsage? Icon => OsuIcon.ModCover; public override LocalisableString Description => @"Decrease the playfield's viewing area."; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs index 2457aa75d7..e6b3541154 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs @@ -1,8 +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.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mods; @@ -13,6 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override string Name => "Dual Stages"; public override string Acronym => "DS"; public override LocalisableString Description => @"Double the stages, double the fun!"; + public override IconUsage? Icon => OsuIcon.ModDualStages; public override ModType Type => ModType.Conversion; public override double ScoreMultiplier => 1; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index f340608fd1..337fd61b91 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -3,7 +3,9 @@ using System; using System.Linq; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Rulesets.Mania.UI; namespace osu.Game.Rulesets.Mania.Mods @@ -12,6 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods { public override string Name => "Fade In"; public override string Acronym => "FI"; + public override IconUsage? Icon => OsuIcon.ModFadeIn; public override LocalisableString Description => @"Keys appear out of nowhere!"; public override double ScoreMultiplier => 1; public override bool ValidForFreestyleAsRequiredMod => false; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs index eba0b2effe..9a1f1948e9 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods; using osu.Framework.Graphics.Sprites; using System.Collections.Generic; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Beatmaps; namespace osu.Game.Rulesets.Mania.Mods @@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override LocalisableString Description => @"Replaces all hold notes with normal notes."; - public override IconUsage? Icon => FontAwesome.Solid.DotCircle; + public override IconUsage? Icon => OsuIcon.ModHoldOff; public override ModType Type => ModType.Conversion; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs index cc407a890f..f0fc9c0685 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mods; @@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override LocalisableString Description => "Hold the keys. To the beat."; - public override IconUsage? Icon => FontAwesome.Solid.YinYang; + public override IconUsage? Icon => OsuIcon.ModInvert; public override ModType Type => ModType.Conversion; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs index 7dd0c499da..290e40fbf3 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Mania.Mods { @@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 1; public override string Name => "One Key"; public override string Acronym => "1K"; + public override IconUsage? Icon => OsuIcon.ModOneKey; public override LocalisableString Description => @"Play with one key."; public override bool Ranked => false; } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs index a6c57d4597..18687148df 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Mania.Mods { @@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 10; public override string Name => "Ten Keys"; public override string Acronym => "10K"; + public override IconUsage? Icon => OsuIcon.ModTenKeys; public override LocalisableString Description => @"Play with ten keys."; public override bool Ranked => false; } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs index 0d04395a52..041d38b98a 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Mania.Mods { @@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 2; public override string Name => "Two Keys"; public override string Acronym => "2K"; + public override IconUsage? Icon => OsuIcon.ModTwoKeys; public override LocalisableString Description => @"Play with two keys."; public override bool Ranked => false; } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs index c83b0979ee..fea5366811 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Mania.Mods { @@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 3; public override string Name => "Three Keys"; public override string Acronym => "3K"; + public override IconUsage? Icon => OsuIcon.ModThreeKeys; public override LocalisableString Description => @"Play with three keys."; public override bool Ranked => false; } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey4.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey4.cs index d3a4546dce..4a9fe7e3df 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey4.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey4.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Mania.Mods { @@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 4; public override string Name => "Four Keys"; public override string Acronym => "4K"; + public override IconUsage? Icon => OsuIcon.ModFourKeys; public override LocalisableString Description => @"Play with four keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey5.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey5.cs index 693182a952..aea2fe9bbe 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey5.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey5.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Mania.Mods { @@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 5; public override string Name => "Five Keys"; public override string Acronym => "5K"; + public override IconUsage? Icon => OsuIcon.ModFiveKeys; public override LocalisableString Description => @"Play with five keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey6.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey6.cs index ab911292f7..e66ea32585 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey6.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey6.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Mania.Mods { @@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 6; public override string Name => "Six Keys"; public override string Acronym => "6K"; + public override IconUsage? Icon => OsuIcon.ModSixKeys; public override LocalisableString Description => @"Play with six keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey7.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey7.cs index ab401ef1d0..07aa60a0a8 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey7.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey7.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Mania.Mods { @@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 7; public override string Name => "Seven Keys"; public override string Acronym => "7K"; + public override IconUsage? Icon => OsuIcon.ModSevenKeys; public override LocalisableString Description => @"Play with seven keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey8.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey8.cs index b3e8a45dda..b6b2016790 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey8.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey8.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Mania.Mods { @@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 8; public override string Name => "Eight Keys"; public override string Acronym => "8K"; + public override IconUsage? Icon => OsuIcon.ModEightKeys; public override LocalisableString Description => @"Play with eight keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey9.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey9.cs index 5972cbf0fe..089bb0402b 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey9.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey9.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Mania.Mods { @@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 9; public override string Name => "Nine Keys"; public override string Acronym => "9K"; + public override IconUsage? Icon => OsuIcon.ModNineKeys; public override LocalisableString Description => @"Play with nine keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs index d664567a63..d72e2ce70c 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs @@ -4,8 +4,10 @@ using System; using System.Linq; using System.Threading; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; @@ -26,6 +28,8 @@ namespace osu.Game.Rulesets.Mania.Mods public override double ScoreMultiplier => 0.9; + public override IconUsage? Icon => OsuIcon.ModNoRelease; + public override ModType Type => ModType.DifficultyReduction; public override Type[] IncompatibleMods => new[] { typeof(ManiaModHoldOff) }; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs index 9bf5d33d4a..d01b561954 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Osu.Mods { @@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Name => @"Alternate"; public override string Acronym => @"AL"; public override LocalisableString Description => @"Don't use the same key twice in a row!"; - public override IconUsage? Icon => FontAwesome.Solid.Keyboard; + public override IconUsage? Icon => OsuIcon.ModAlternate; public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray(); protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction != action; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs index f213d9f193..033ab0f861 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -19,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Acronym => "AD"; public override LocalisableString Description => "Never trust the approach circles..."; public override double ScoreMultiplier => 1; - public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle; + public override IconUsage? Icon => OsuIcon.ModApproachDifferent; public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModFreezeFrame) }; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs index bb0e984418..97d76459c6 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; @@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override LocalisableString Description => "Play with blinds on your screen."; public override string Acronym => "BL"; - public override IconUsage? Icon => FontAwesome.Solid.Adjust; + public override IconUsage? Icon => OsuIcon.ModBlinds; public override ModType Type => ModType.DifficultyIncrease; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBloom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBloom.cs index c674074dc6..445fb8b37a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModBloom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModBloom.cs @@ -3,9 +3,11 @@ using System; using osu.Framework.Bindables; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Mods; @@ -21,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Name => "Bloom"; public override string Acronym => "BM"; + public override IconUsage? Icon => OsuIcon.ModBloom; public override ModType Type => ModType.Fun; public override LocalisableString Description => "The cursor blooms into.. a larger cursor!"; public override double ScoreMultiplier => 1; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs index b34cc29741..b706e07a55 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs @@ -11,7 +11,9 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; @@ -34,6 +36,8 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; + public override IconUsage? Icon => OsuIcon.ModBubbles; + public override ModType Type => ModType.Fun; // Compatibility with these seems potentially feasible in the future, blocked for now because they don't work as one would expect diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs index f6622c268d..faceb2ac7c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs @@ -4,6 +4,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Osu.Mods { @@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Acronym => "DF"; - public override IconUsage? Icon => FontAwesome.Solid.CompressArrowsAlt; + public override IconUsage? Icon => OsuIcon.ModDeflate; public override LocalisableString Description => "Hit them at the right size!"; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs index 306dcee839..ea0be78c09 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; @@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Name => "Depth"; public override string Acronym => "DP"; - public override IconUsage? Icon => FontAwesome.Solid.Cube; + public override IconUsage? Icon => OsuIcon.ModDepth; public override ModType Type => ModType.Fun; public override LocalisableString Description => "3D. Almost."; public override double ScoreMultiplier => 1; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs index 421b908dc9..e75ed24a7d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs @@ -4,8 +4,10 @@ using System; using System.Linq; using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; @@ -19,6 +21,8 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Acronym => "FR"; + public override IconUsage? Icon => OsuIcon.ModFreezeFrame; + public override double ScoreMultiplier => 1; public override LocalisableString Description => "Burn the notes into your memory."; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs index c2b6556090..475089ba94 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs @@ -4,6 +4,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Osu.Mods { @@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Acronym => "GR"; - public override IconUsage? Icon => FontAwesome.Solid.ArrowsAltV; + public override IconUsage? Icon => OsuIcon.ModGrow; public override LocalisableString Description => "Hit them at the right size!"; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index 5038250261..11b512c882 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -9,6 +9,7 @@ using osu.Framework.Localisation; using osu.Framework.Timing; using osu.Framework.Utils; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; @@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Name => "Magnetised"; public override string Acronym => "MG"; - public override IconUsage? Icon => FontAwesome.Solid.Magnet; + public override IconUsage? Icon => OsuIcon.ModMagnetised; public override ModType Type => ModType.Fun; public override LocalisableString Description => "No need to chase the circles – your cursor is a magnet!"; public override double ScoreMultiplier => 0.5; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs index 1d58e0f102..b95cc9b651 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs @@ -4,10 +4,12 @@ using System; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Timing; using osu.Framework.Utils; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; @@ -23,6 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Name => "Repel"; public override string Acronym => "RP"; + public override IconUsage? Icon => OsuIcon.ModRepel; public override ModType Type => ModType.Fun; public override LocalisableString Description => "Hit objects run away!"; public override double ScoreMultiplier => 1; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs index 91731b25cf..6d16598f89 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs @@ -3,7 +3,9 @@ using System; using System.Linq; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Osu.Mods { @@ -11,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Name => @"Single Tap"; public override string Acronym => @"SG"; + public override IconUsage? Icon => OsuIcon.ModSingleTap; public override LocalisableString Description => @"You must only use one key!"; public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray(); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs index 59a1342480..429332bc55 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; @@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Name => "Spin In"; public override string Acronym => "SI"; - public override IconUsage? Icon => FontAwesome.Solid.Undo; + public override IconUsage? Icon => OsuIcon.ModSpinIn; public override ModType Type => ModType.Fun; public override LocalisableString Description => "Circles spin in. No approach circles."; public override double ScoreMultiplier => 1; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index 129c03149f..16ef639384 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -4,8 +4,10 @@ using System; using System.Linq; using System.Threading; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -24,6 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Name => @"Strict Tracking"; public override string Acronym => @"ST"; + public override IconUsage? Icon => OsuIcon.ModStrictTracking; public override ModType Type => ModType.DifficultyIncrease; public override LocalisableString Description => @"Once you start a slider, follow precisely or get a miss."; public override double ScoreMultiplier => 1.0; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs index 71080e3d8e..e82ec2fb10 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Name => "Target Practice"; public override string Acronym => "TP"; public override ModType Type => ModType.Conversion; - public override IconUsage? Icon => OsuIcon.ModTarget; + public override IconUsage? Icon => OsuIcon.ModTargetPractice; public override LocalisableString Description => @"Practice keeping up with the beat of the song."; public override double ScoreMultiplier => 0.1; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index 9091837034..b2a3da285c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -4,7 +4,9 @@ using System; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -18,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Name => "Traceable"; public override string Acronym => "TC"; + public override IconUsage? Icon => OsuIcon.ModTraceable; public override ModType Type => ModType.Fun; public override LocalisableString Description => "Put your faith in the approach circles..."; public override double ScoreMultiplier => 1; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs index 2a58168edc..d0a1350db9 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; @@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Name => "Transform"; public override string Acronym => "TR"; - public override IconUsage? Icon => FontAwesome.Solid.ArrowsAlt; + public override IconUsage? Icon => OsuIcon.ModTransform; public override ModType Type => ModType.Fun; public override LocalisableString Description => "Everything rotates. EVERYTHING."; public override double ScoreMultiplier => 1; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs index b7413c893c..7c0faab235 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; @@ -19,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Name => "Wiggle"; public override string Acronym => "WG"; - public override IconUsage? Icon => FontAwesome.Solid.Certificate; + public override IconUsage? Icon => OsuIcon.ModWiggle; public override ModType Type => ModType.Fun; public override LocalisableString Description => "They just won't stay still..."; public override double ScoreMultiplier => 1; diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs index 81973e65cc..e6fd5fc93e 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.Mods; @@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Mods public override string Acronym => "CS"; public override double ScoreMultiplier => 0.9; public override LocalisableString Description => "No more tricky speed changes!"; - public override IconUsage? Icon => FontAwesome.Solid.Equals; + public override IconUsage? Icon => OsuIcon.ModConstantSpeed; public override ModType Type => ModType.Conversion; public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs index 6e9b974fbf..2132121cd2 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs @@ -5,10 +5,12 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Objects; @@ -21,6 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Mods public override string Acronym => "SR"; public override double ScoreMultiplier => 0.6; public override LocalisableString Description => "Simplify tricky rhythms!"; + public override IconUsage? Icon => OsuIcon.ModSimplifiedRhythm; public override ModType Type => ModType.DifficultyReduction; [SettingSource("1/3 to 1/2 conversion", "Converts 1/3 patterns to 1/2 rhythm.")] diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs index 511278dab0..43c8708565 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs @@ -6,9 +6,11 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Beatmaps.Timing; +using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; @@ -24,6 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { public override string Name => @"Single Tap"; public override string Acronym => @"SG"; + public override IconUsage? Icon => OsuIcon.ModSingleTap; public override LocalisableString Description => @"One key for dons, one key for kats."; public override double ScoreMultiplier => 1.0; diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs index fc3913f56d..f1feb8153a 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs @@ -3,8 +3,10 @@ using System; using System.Linq; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Objects; @@ -16,6 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Mods public override string Name => "Swap"; public override string Acronym => "SW"; public override LocalisableString Description => @"Dons become kats, kats become dons"; + public override IconUsage? Icon => OsuIcon.ModSwap; public override ModType Type => ModType.Conversion; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModRandom)).ToArray(); diff --git a/osu.Game/Graphics/OsuIcon.cs b/osu.Game/Graphics/OsuIcon.cs index 84ff86a5e5..3a8dfac826 100644 --- a/osu.Game/Graphics/OsuIcon.cs +++ b/osu.Game/Graphics/OsuIcon.cs @@ -81,27 +81,6 @@ namespace osu.Game.Graphics public static IconUsage InsaneMania => get(0xe027); public static IconUsage ExpertMania => get(0xe028); - // mod icons - public static IconUsage ModPerfect => get(0xe049); - public static IconUsage ModAutopilot => get(0xe03a); - public static IconUsage ModAuto => get(0xe03b); - public static IconUsage ModCinema => get(0xe03c); - public static IconUsage ModDoubleTime => get(0xe03d); - public static IconUsage ModEasy => get(0xe03e); - public static IconUsage ModFlashlight => get(0xe03f); - public static IconUsage ModHalftime => get(0xe040); - public static IconUsage ModHardRock => get(0xe041); - public static IconUsage ModHidden => get(0xe042); - public static IconUsage ModNightcore => get(0xe043); - public static IconUsage ModNoFail => get(0xe044); - public static IconUsage ModRelax => get(0xe045); - public static IconUsage ModSpunOut => get(0xe046); - public static IconUsage ModSuddenDeath => get(0xe047); - public static IconUsage ModTarget => get(0xe048); - - // Use "Icons/BeatmapDetails/mod-icon" instead - // public static IconUsage ModBg => Get(0xe04a); - #endregion #region New single-file-based icons @@ -181,6 +160,88 @@ namespace osu.Game.Graphics public static IconUsage Tortoise => get(OsuIconMapping.Tortoise); public static IconUsage Hare => get(OsuIconMapping.Hare); + // mod icons + + public static IconUsage ModNoMod => get(OsuIconMapping.ModNoMod); + + /* + can be regenerated semi-automatically using osu-web's mod database via + + $ jq -r '.[].Mods[].Name' mods.json | sort | uniq | \ + sed 's/ //g' | \ + awk '{print "public static IconUsage Mod" $0 " => get(OsuIconMapping.Mod" $0 ");"}' | pbcopy + */ + + public static IconUsage ModAccuracyChallenge => get(OsuIconMapping.ModAccuracyChallenge); + public static IconUsage ModAdaptiveSpeed => get(OsuIconMapping.ModAdaptiveSpeed); + public static IconUsage ModAlternate => get(OsuIconMapping.ModAlternate); + public static IconUsage ModApproachDifferent => get(OsuIconMapping.ModApproachDifferent); + public static IconUsage ModAutopilot => get(OsuIconMapping.ModAutopilot); + public static IconUsage ModAutoplay => get(OsuIconMapping.ModAutoplay); + public static IconUsage ModBarrelRoll => get(OsuIconMapping.ModBarrelRoll); + public static IconUsage ModBlinds => get(OsuIconMapping.ModBlinds); + public static IconUsage ModBloom => get(OsuIconMapping.ModBloom); + public static IconUsage ModBubbles => get(OsuIconMapping.ModBubbles); + public static IconUsage ModCinema => get(OsuIconMapping.ModCinema); + public static IconUsage ModClassic => get(OsuIconMapping.ModClassic); + public static IconUsage ModConstantSpeed => get(OsuIconMapping.ModConstantSpeed); + public static IconUsage ModCover => get(OsuIconMapping.ModCover); + public static IconUsage ModDaycore => get(OsuIconMapping.ModDaycore); + public static IconUsage ModDeflate => get(OsuIconMapping.ModDeflate); + public static IconUsage ModDepth => get(OsuIconMapping.ModDepth); + public static IconUsage ModDifficultyAdjust => get(OsuIconMapping.ModDifficultyAdjust); + public static IconUsage ModDoubleTime => get(OsuIconMapping.ModDoubleTime); + public static IconUsage ModDualStages => get(OsuIconMapping.ModDualStages); + public static IconUsage ModEasy => get(OsuIconMapping.ModEasy); + public static IconUsage ModEightKeys => get(OsuIconMapping.ModEightKeys); + public static IconUsage ModFadeIn => get(OsuIconMapping.ModFadeIn); + public static IconUsage ModFiveKeys => get(OsuIconMapping.ModFiveKeys); + public static IconUsage ModFlashlight => get(OsuIconMapping.ModFlashlight); + public static IconUsage ModFloatingFruits => get(OsuIconMapping.ModFloatingFruits); + public static IconUsage ModFourKeys => get(OsuIconMapping.ModFourKeys); + public static IconUsage ModFreezeFrame => get(OsuIconMapping.ModFreezeFrame); + public static IconUsage ModGrow => get(OsuIconMapping.ModGrow); + public static IconUsage ModHalfTime => get(OsuIconMapping.ModHalfTime); + public static IconUsage ModHardRock => get(OsuIconMapping.ModHardRock); + public static IconUsage ModHidden => get(OsuIconMapping.ModHidden); + public static IconUsage ModHoldOff => get(OsuIconMapping.ModHoldOff); + public static IconUsage ModInvert => get(OsuIconMapping.ModInvert); + public static IconUsage ModMagnetised => get(OsuIconMapping.ModMagnetised); + public static IconUsage ModMirror => get(OsuIconMapping.ModMirror); + public static IconUsage ModMovingFast => get(OsuIconMapping.ModMovingFast); + public static IconUsage ModMuted => get(OsuIconMapping.ModMuted); + public static IconUsage ModNightcore => get(OsuIconMapping.ModNightcore); + public static IconUsage ModNineKeys => get(OsuIconMapping.ModNineKeys); + public static IconUsage ModNoFail => get(OsuIconMapping.ModNoFail); + public static IconUsage ModNoRelease => get(OsuIconMapping.ModNoRelease); + public static IconUsage ModNoScope => get(OsuIconMapping.ModNoScope); + public static IconUsage ModOneKey => get(OsuIconMapping.ModOneKey); + public static IconUsage ModPerfect => get(OsuIconMapping.ModPerfect); + public static IconUsage ModRandom => get(OsuIconMapping.ModRandom); + public static IconUsage ModRelax => get(OsuIconMapping.ModRelax); + public static IconUsage ModRepel => get(OsuIconMapping.ModRepel); + public static IconUsage ModScoreV2 => get(OsuIconMapping.ModScoreV2); + public static IconUsage ModSevenKeys => get(OsuIconMapping.ModSevenKeys); + public static IconUsage ModSimplifiedRhythm => get(OsuIconMapping.ModSimplifiedRhythm); + public static IconUsage ModSingleTap => get(OsuIconMapping.ModSingleTap); + public static IconUsage ModSixKeys => get(OsuIconMapping.ModSixKeys); + public static IconUsage ModSpinIn => get(OsuIconMapping.ModSpinIn); + public static IconUsage ModSpunOut => get(OsuIconMapping.ModSpunOut); + public static IconUsage ModStrictTracking => get(OsuIconMapping.ModStrictTracking); + public static IconUsage ModSuddenDeath => get(OsuIconMapping.ModSuddenDeath); + public static IconUsage ModSwap => get(OsuIconMapping.ModSwap); + public static IconUsage ModSynesthesia => get(OsuIconMapping.ModSynesthesia); + public static IconUsage ModTargetPractice => get(OsuIconMapping.ModTargetPractice); + public static IconUsage ModTenKeys => get(OsuIconMapping.ModTenKeys); + public static IconUsage ModThreeKeys => get(OsuIconMapping.ModThreeKeys); + public static IconUsage ModTouchDevice => get(OsuIconMapping.ModTouchDevice); + public static IconUsage ModTraceable => get(OsuIconMapping.ModTraceable); + public static IconUsage ModTransform => get(OsuIconMapping.ModTransform); + public static IconUsage ModTwoKeys => get(OsuIconMapping.ModTwoKeys); + public static IconUsage ModWiggle => get(OsuIconMapping.ModWiggle); + public static IconUsage ModWindDown => get(OsuIconMapping.ModWindDown); + public static IconUsage ModWindUp => get(OsuIconMapping.ModWindUp); + private static IconUsage get(OsuIconMapping glyph) => new IconUsage((char)glyph, FONT_NAME); private enum OsuIconMapping @@ -400,6 +461,224 @@ namespace osu.Game.Graphics [Description(@"hare")] Hare, + + // mod icons + + [Description(@"Mods/mod-no-mod")] + ModNoMod, + + /* + rest can be regenerated semi-automatically using osu-web's mod database via + $ jq -r '.[].Mods[].Name' mods.json | sort | uniq | \ + awk '{kebab = $0; gsub(" ", "-", kebab); pascal = $0; gsub(" ", "", pascal); print "[Description(@\"Mods/mod-" tolower(kebab) "\")]\nMod" pascal ",\n" }' | pbcopy + */ + + [Description(@"Mods/mod-accuracy-challenge")] + ModAccuracyChallenge, + + [Description(@"Mods/mod-adaptive-speed")] + ModAdaptiveSpeed, + + [Description(@"Mods/mod-alternate")] + ModAlternate, + + [Description(@"Mods/mod-approach-different")] + ModApproachDifferent, + + [Description(@"Mods/mod-autopilot")] + ModAutopilot, + + [Description(@"Mods/mod-autoplay")] + ModAutoplay, + + [Description(@"Mods/mod-barrel-roll")] + ModBarrelRoll, + + [Description(@"Mods/mod-blinds")] + ModBlinds, + + [Description(@"Mods/mod-bloom")] + ModBloom, + + [Description(@"Mods/mod-bubbles")] + ModBubbles, + + [Description(@"Mods/mod-cinema")] + ModCinema, + + [Description(@"Mods/mod-classic")] + ModClassic, + + [Description(@"Mods/mod-constant-speed")] + ModConstantSpeed, + + [Description(@"Mods/mod-cover")] + ModCover, + + [Description(@"Mods/mod-daycore")] + ModDaycore, + + [Description(@"Mods/mod-deflate")] + ModDeflate, + + [Description(@"Mods/mod-depth")] + ModDepth, + + [Description(@"Mods/mod-difficulty-adjust")] + ModDifficultyAdjust, + + [Description(@"Mods/mod-double-time")] + ModDoubleTime, + + [Description(@"Mods/mod-dual-stages")] + ModDualStages, + + [Description(@"Mods/mod-easy")] + ModEasy, + + [Description(@"Mods/mod-eight-keys")] + ModEightKeys, + + [Description(@"Mods/mod-fade-in")] + ModFadeIn, + + [Description(@"Mods/mod-five-keys")] + ModFiveKeys, + + [Description(@"Mods/mod-flashlight")] + ModFlashlight, + + [Description(@"Mods/mod-floating-fruits")] + ModFloatingFruits, + + [Description(@"Mods/mod-four-keys")] + ModFourKeys, + + [Description(@"Mods/mod-freeze-frame")] + ModFreezeFrame, + + [Description(@"Mods/mod-grow")] + ModGrow, + + [Description(@"Mods/mod-half-time")] + ModHalfTime, + + [Description(@"Mods/mod-hard-rock")] + ModHardRock, + + [Description(@"Mods/mod-hidden")] + ModHidden, + + [Description(@"Mods/mod-hold-off")] + ModHoldOff, + + [Description(@"Mods/mod-invert")] + ModInvert, + + [Description(@"Mods/mod-magnetised")] + ModMagnetised, + + [Description(@"Mods/mod-mirror")] + ModMirror, + + [Description(@"Mods/mod-moving-fast")] + ModMovingFast, + + [Description(@"Mods/mod-muted")] + ModMuted, + + [Description(@"Mods/mod-nightcore")] + ModNightcore, + + [Description(@"Mods/mod-nine-keys")] + ModNineKeys, + + [Description(@"Mods/mod-no-fail")] + ModNoFail, + + [Description(@"Mods/mod-no-release")] + ModNoRelease, + + [Description(@"Mods/mod-no-scope")] + ModNoScope, + + [Description(@"Mods/mod-one-key")] + ModOneKey, + + [Description(@"Mods/mod-perfect")] + ModPerfect, + + [Description(@"Mods/mod-random")] + ModRandom, + + [Description(@"Mods/mod-relax")] + ModRelax, + + [Description(@"Mods/mod-repel")] + ModRepel, + + [Description(@"Mods/mod-score-v2")] + ModScoreV2, + + [Description(@"Mods/mod-seven-keys")] + ModSevenKeys, + + [Description(@"Mods/mod-simplified-rhythm")] + ModSimplifiedRhythm, + + [Description(@"Mods/mod-single-tap")] + ModSingleTap, + + [Description(@"Mods/mod-six-keys")] + ModSixKeys, + + [Description(@"Mods/mod-spin-in")] + ModSpinIn, + + [Description(@"Mods/mod-spun-out")] + ModSpunOut, + + [Description(@"Mods/mod-strict-tracking")] + ModStrictTracking, + + [Description(@"Mods/mod-sudden-death")] + ModSuddenDeath, + + [Description(@"Mods/mod-swap")] + ModSwap, + + [Description(@"Mods/mod-synesthesia")] + ModSynesthesia, + + [Description(@"Mods/mod-target-practice")] + ModTargetPractice, + + [Description(@"Mods/mod-ten-keys")] + ModTenKeys, + + [Description(@"Mods/mod-three-keys")] + ModThreeKeys, + + [Description(@"Mods/mod-touch-device")] + ModTouchDevice, + + [Description(@"Mods/mod-traceable")] + ModTraceable, + + [Description(@"Mods/mod-transform")] + ModTransform, + + [Description(@"Mods/mod-two-keys")] + ModTwoKeys, + + [Description(@"Mods/mod-wiggle")] + ModWiggle, + + [Description(@"Mods/mod-wind-down")] + ModWindDown, + + [Description(@"Mods/mod-wind-up")] + ModWindUp, } public class OsuIconStore : ITextureStore, ITexturedGlyphLookupStore diff --git a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs index db16e771d3..f26a1bd477 100644 --- a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs +++ b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs @@ -6,8 +6,10 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Localisation.HUD; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Scoring; @@ -24,6 +26,8 @@ namespace osu.Game.Rulesets.Mods public override LocalisableString Description => "Fail if your accuracy drops too low!"; + public override IconUsage? Icon => OsuIcon.ModAccuracyChallenge; + public override ModType Type => ModType.DifficultyIncrease; public override double ScoreMultiplier => 1.0; diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 83a48599ca..63d2f7d7f3 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -6,10 +6,12 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -27,6 +29,8 @@ namespace osu.Game.Rulesets.Mods public override LocalisableString Description => "Let track speed adapt to you."; + public override IconUsage? Icon => OsuIcon.ModAdaptiveSpeed; + public override ModType Type => ModType.Fun; public override double ScoreMultiplier => 0.5; diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 302cdf69c0..01e01a0d9a 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mods { public override string Name => "Autoplay"; public override string Acronym => "AT"; - public override IconUsage? Icon => OsuIcon.ModAuto; + public override IconUsage? Icon => OsuIcon.ModAutoplay; public override ModType Type => ModType.Automation; public override LocalisableString Description => "Watch a perfect automated play through the song."; public override double ScoreMultiplier => 1; diff --git a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs index ceaa9aa6e5..22d2f41b82 100644 --- a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs +++ b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs @@ -6,8 +6,10 @@ using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; using osuTK; @@ -36,6 +38,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => "Barrel Roll"; public override string Acronym => "BR"; + public override IconUsage? Icon => OsuIcon.ModBarrelRoll; public override LocalisableString Description => "The whole playfield is on a wheel!"; public override double ScoreMultiplier => 1; diff --git a/osu.Game/Rulesets/Mods/ModClassic.cs b/osu.Game/Rulesets/Mods/ModClassic.cs index 66d6ea2e66..e8c6bd09c1 100644 --- a/osu.Game/Rulesets/Mods/ModClassic.cs +++ b/osu.Game/Rulesets/Mods/ModClassic.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Mods { @@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 0.96; - public override IconUsage? Icon => FontAwesome.Solid.History; + public override IconUsage? Icon => OsuIcon.ModClassic; public override LocalisableString Description => "Feeling nostalgic?"; diff --git a/osu.Game/Rulesets/Mods/ModDaycore.cs b/osu.Game/Rulesets/Mods/ModDaycore.cs index 359f8a950c..98ecf0d46a 100644 --- a/osu.Game/Rulesets/Mods/ModDaycore.cs +++ b/osu.Game/Rulesets/Mods/ModDaycore.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Overlays.Settings; namespace osu.Game.Rulesets.Mods @@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mods { public override string Name => "Daycore"; public override string Acronym => "DC"; - public override IconUsage? Icon => null; + public override IconUsage? Icon => OsuIcon.ModDaycore; public override ModType Type => ModType.DifficultyReduction; public override LocalisableString Description => "Whoaaaaa..."; public override bool Ranked => UsesDefaultConfiguration; diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index dbc690bd15..da5f5df200 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -9,6 +9,7 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Extensions; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Mods { @@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.Conversion; - public override IconUsage? Icon => FontAwesome.Solid.Hammer; + public override IconUsage? Icon => OsuIcon.ModDifficultyAdjust; public override double ScoreMultiplier => 0.5; diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index e2790e9c22..c91c8b2718 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mods { public override string Name => "Half Time"; public override string Acronym => "HT"; - public override IconUsage? Icon => OsuIcon.ModHalftime; + public override IconUsage? Icon => OsuIcon.ModHalfTime; public override ModType Type => ModType.DifficultyReduction; public override LocalisableString Description => "Less zoom..."; public override bool Ranked => SpeedChange.IsDefault; diff --git a/osu.Game/Rulesets/Mods/ModMirror.cs b/osu.Game/Rulesets/Mods/ModMirror.cs index 3c4b7d0c60..c2e21c6770 100644 --- a/osu.Game/Rulesets/Mods/ModMirror.cs +++ b/osu.Game/Rulesets/Mods/ModMirror.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 osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; + namespace osu.Game.Rulesets.Mods { public abstract class ModMirror : Mod { public override string Name => "Mirror"; public override string Acronym => "MR"; + public override IconUsage? Icon => OsuIcon.ModMirror; public override ModType Type => ModType.Conversion; public override double ScoreMultiplier => 1; } diff --git a/osu.Game/Rulesets/Mods/ModMuted.cs b/osu.Game/Rulesets/Mods/ModMuted.cs index 2eb243d565..933e7f4093 100644 --- a/osu.Game/Rulesets/Mods/ModMuted.cs +++ b/osu.Game/Rulesets/Mods/ModMuted.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Objects; @@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Mods { public override string Name => "Muted"; public override string Acronym => "MU"; - public override IconUsage? Icon => FontAwesome.Solid.VolumeMute; + public override IconUsage? Icon => OsuIcon.ModMuted; public override LocalisableString Description => "Can you still feel the rhythm without music?"; public override ModType Type => ModType.Fun; public override double ScoreMultiplier => 1; diff --git a/osu.Game/Rulesets/Mods/ModNoMod.cs b/osu.Game/Rulesets/Mods/ModNoMod.cs index 5dd4b317e7..0f55ab126f 100644 --- a/osu.Game/Rulesets/Mods/ModNoMod.cs +++ b/osu.Game/Rulesets/Mods/ModNoMod.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Mods { @@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Mods public override string Acronym => "NM"; public override LocalisableString Description => "No mods applied."; public override double ScoreMultiplier => 1; - public override IconUsage? Icon => FontAwesome.Solid.Ban; + public override IconUsage? Icon => OsuIcon.ModNoMod; public override ModType Type => ModType.System; } } diff --git a/osu.Game/Rulesets/Mods/ModNoScope.cs b/osu.Game/Rulesets/Mods/ModNoScope.cs index dd1bd9a719..d0c9da669b 100644 --- a/osu.Game/Rulesets/Mods/ModNoScope.cs +++ b/osu.Game/Rulesets/Mods/ModNoScope.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Scoring; @@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => "No Scope"; public override string Acronym => "NS"; public override ModType Type => ModType.Fun; - public override IconUsage? Icon => FontAwesome.Solid.EyeSlash; + public override IconUsage? Icon => OsuIcon.ModNoScope; public override double ScoreMultiplier => 1; public override bool Ranked => true; diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs index 178b9fb619..684caa2a3f 100644 --- a/osu.Game/Rulesets/Mods/ModRandom.cs +++ b/osu.Game/Rulesets/Mods/ModRandom.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => "Random"; public override string Acronym => "RD"; public override ModType Type => ModType.Conversion; - public override IconUsage? Icon => OsuIcon.Dice; + public override IconUsage? Icon => OsuIcon.ModRandom; public override double ScoreMultiplier => 1; [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))] diff --git a/osu.Game/Rulesets/Mods/ModScoreV2.cs b/osu.Game/Rulesets/Mods/ModScoreV2.cs index 854f3916a1..dce6e146df 100644 --- a/osu.Game/Rulesets/Mods/ModScoreV2.cs +++ b/osu.Game/Rulesets/Mods/ModScoreV2.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Mods { @@ -13,6 +15,7 @@ namespace osu.Game.Rulesets.Mods { public override string Name => "Score V2"; public override string Acronym => @"SV2"; + public override IconUsage? Icon => OsuIcon.ModScoreV2; public override ModType Type => ModType.System; public override LocalisableString Description => "Score set on earlier osu! versions with the V2 scoring algorithm active."; public override double ScoreMultiplier => 1; diff --git a/osu.Game/Rulesets/Mods/ModSynesthesia.cs b/osu.Game/Rulesets/Mods/ModSynesthesia.cs index 9084127f33..31ff7ca3fe 100644 --- a/osu.Game/Rulesets/Mods/ModSynesthesia.cs +++ b/osu.Game/Rulesets/Mods/ModSynesthesia.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Mods { @@ -14,6 +16,7 @@ namespace osu.Game.Rulesets.Mods public override string Acronym => "SY"; public override LocalisableString Description => "Colours hit objects based on the rhythm."; public override double ScoreMultiplier => 0.8; + public override IconUsage? Icon => OsuIcon.ModSynesthesia; public override ModType Type => ModType.Fun; } } diff --git a/osu.Game/Rulesets/Mods/ModTouchDevice.cs b/osu.Game/Rulesets/Mods/ModTouchDevice.cs index e91a398700..f5e6fc03bf 100644 --- a/osu.Game/Rulesets/Mods/ModTouchDevice.cs +++ b/osu.Game/Rulesets/Mods/ModTouchDevice.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mods { public sealed override string Name => "Touch Device"; public sealed override string Acronym => "TD"; - public sealed override IconUsage? Icon => OsuIcon.PlayStyleTouch; + public sealed override IconUsage? Icon => OsuIcon.ModTouchDevice; public sealed override LocalisableString Description => "Automatically applied to plays on devices with a touchscreen."; public sealed override double ScoreMultiplier => 1; public sealed override ModType Type => ModType.System; diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs index 35a673093b..cad16ab3bb 100644 --- a/osu.Game/Rulesets/Mods/ModWindDown.cs +++ b/osu.Game/Rulesets/Mods/ModWindDown.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Mods { @@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => "Wind Down"; public override string Acronym => "WD"; public override LocalisableString Description => "Sloooow doooown..."; - public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleDown; + public override IconUsage? Icon => OsuIcon.ModWindDown; public override BindableNumber InitialRate { get; } = new BindableDouble(1) { diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs index bbc8382055..42555137b5 100644 --- a/osu.Game/Rulesets/Mods/ModWindUp.cs +++ b/osu.Game/Rulesets/Mods/ModWindUp.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Mods { @@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => "Wind Up"; public override string Acronym => "WU"; public override LocalisableString Description => "Can you keep up?"; - public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleUp; + public override IconUsage? Icon => OsuIcon.ModWindUp; public override BindableNumber InitialRate { get; } = new BindableDouble(1) { From e47a60f30368fc8d6c4488216da4c334fc4fa8c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 20 Aug 2025 14:01:46 +0200 Subject: [PATCH 05/57] Add test steps to mod icon test scene for exercising all rulesets --- .../Visual/UserInterface/TestSceneModIcon.cs | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs index c18f00677d..d554ac7424 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs @@ -4,11 +4,13 @@ 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.Graphics.Sprites; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -22,6 +24,9 @@ namespace osu.Game.Tests.Visual.UserInterface private FillFlowContainer spreadOutFlow = null!; private ModDisplay modDisplay = null!; + [Resolved] + private RulesetStore rulesetStore { get; set; } = null!; + [SetUpSteps] public void SetUpSteps() { @@ -70,9 +75,26 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestShowAllMods() { - AddStep("create mod icons", () => + createModIconsForRuleset(0); + createModIconsForRuleset(1); + createModIconsForRuleset(2); + createModIconsForRuleset(3); + + AddStep("toggle selected", () => { - addRange(Ruleset.Value.CreateInstance().CreateAllMods().Select(m => + foreach (var icon in this.ChildrenOfType()) + icon.Selected.Toggle(); + }); + } + + private void createModIconsForRuleset(int rulesetId) + { + AddStep($"create mod icons for ruleset {rulesetId}", () => + { + spreadOutFlow.Clear(); + modDisplay.Current.Value = []; + + addRange(rulesetStore.GetRuleset(rulesetId)!.CreateInstance().CreateAllMods().Select(m => { if (m is OsuModFlashlight fl) fl.FollowDelay.Value = 1245; @@ -89,12 +111,6 @@ namespace osu.Game.Tests.Visual.UserInterface return m; })); }); - - AddStep("toggle selected", () => - { - foreach (var icon in this.ChildrenOfType()) - icon.Selected.Toggle(); - }); } [Test] From c053cfbf9b230426853a6a6081566bae2a4930b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 20 Aug 2025 14:01:55 +0200 Subject: [PATCH 06/57] Adjust icon sizings in mod display to match new assets --- osu.Game/Rulesets/UI/ModIcon.cs | 8 +++++++- osu.Game/Rulesets/UI/ModSwitchSmall.cs | 5 +++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 9ed4f7135f..79cf073a42 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -167,7 +167,13 @@ namespace osu.Game.Rulesets.UI { Origin = Anchor.Centre, Anchor = Anchor.Centre, - Size = new Vector2(45), + RelativeSizeAxes = Axes.Both, + // the mod icon assets in `osu-resources` are sized such that they are flush with the hexagonal background with no shadow baked in. + // the `Icons/BeatmapDetails/mod-icon` asset (of size 135x100) has a shadow and some extra transparent pixels baked in. + // the hexagonal background on that asset, excluding its shadow and the transparent pixels, is 131px wide and 92px high. + // height is divided by 135 rather than by 100, because this entire component is square-sized. + Width = 131 / 135f, + Height = 92 / 135f, Icon = FontAwesome.Solid.Question }, adjustmentMarker = new Container diff --git a/osu.Game/Rulesets/UI/ModSwitchSmall.cs b/osu.Game/Rulesets/UI/ModSwitchSmall.cs index 6e96cc8e6f..c471a7f3f2 100644 --- a/osu.Game/Rulesets/UI/ModSwitchSmall.cs +++ b/osu.Game/Rulesets/UI/ModSwitchSmall.cs @@ -61,7 +61,6 @@ namespace osu.Game.Rulesets.UI AutoSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Spacing = new Vector2(0, 4), Direction = FillDirection.Vertical, Child = tinySwitch = new ModSwitchTiny(mod) { @@ -79,7 +78,9 @@ namespace osu.Game.Rulesets.UI { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Size = new Vector2(21), + Size = new Vector2(37, 26), + // arbitrary adjustment for better vertical alignment + Margin = new MarginPadding { Top = -1 }, Icon = mod.Icon.Value }); tinySwitch.Scale = new Vector2(0.3f); From a7f1795f980896ec6d8ce0fdd8cfd2f797be5574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 21 Aug 2025 11:21:31 +0200 Subject: [PATCH 07/57] Fix song select background being stuck in revealed state Closes https://github.com/ppy/osu/issues/34731. The failure scenario here is as follows: - User holds down left mouse button for >200ms to reveal the background. - User presses down another mouse button and releases it in <200ms. - User releases left mouse button. Song select does not return. The timing here is key because what is happening here is that the second mouse button press is overwriting the `revealingBackground` scheduled delegate. Releasing that same mouse button within 200ms leads to that scheduled delegate being cancelled and cleared, and thus the release of left mouse wrongly decides there is nothing left to do. One thing I'm not entirely sure about is the release behaviour even with this change; as things stand, the first release of any mouse button will bring song select back, even if it was not the button that was initially held down to reveal the background. That's probably easily fixed if deemed required, but I'm most interested in fixing the bad breakage. --- osu.Game/Screens/SelectV2/SongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 7e99efe987..4ff571a3f8 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -850,7 +850,7 @@ namespace osu.Game.Screens.SelectV2 // For simplicity, disable this functionality on mobile. bool isTouchInput = e.CurrentState.Mouse.LastSource is ISourcedFromTouch; - if (!carousel.AbsoluteScrolling && !isTouchInput && mouseDownPriority) + if (!carousel.AbsoluteScrolling && !isTouchInput && mouseDownPriority && revealingBackground == null) { revealingBackground = Scheduler.AddDelayed(() => { From 4627c8a8591589713e4ddbc964f1873d3bb347cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 21 Aug 2025 14:44:43 +0200 Subject: [PATCH 08/57] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index fbabb3c178..8093e49d1c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From a049f5065d2f0f209264f4f1299dc63fd2802cf4 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 20 Aug 2025 15:45:56 +0300 Subject: [PATCH 09/57] Fix flashlight not correctly scaled to match playfield --- osu.Game/Rulesets/Mods/ModFlashlight.cs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index a88d714dce..6b24e8b77f 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Runtime.InteropServices; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -15,7 +14,6 @@ using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Shaders.Types; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; -using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.OpenGL.Vertices; @@ -85,7 +83,7 @@ namespace osu.Game.Rulesets.Mods flashlight.Colour = Color4.Black; flashlight.Combo.BindTo(Combo); - flashlight.GetPlayfieldScale = () => drawableRuleset.PlayfieldAdjustmentContainer.Scale; + flashlight.Playfield = drawableRuleset.Playfield; drawableRuleset.Overlays.Add(new Container { @@ -111,7 +109,7 @@ namespace osu.Game.Rulesets.Mods public override bool RemoveCompletedTransforms => false; - internal Func? GetPlayfieldScale; + internal Playfield Playfield { get; set; } = null!; private readonly float defaultFlashlightSize; private readonly float sizeMultiplier; @@ -156,15 +154,6 @@ namespace osu.Game.Rulesets.Mods { float size = defaultFlashlightSize * sizeMultiplier; - if (GetPlayfieldScale != null) - { - Vector2 playfieldScale = GetPlayfieldScale(); - - Debug.Assert(Precision.AlmostEquals(Math.Abs(playfieldScale.X), Math.Abs(playfieldScale.Y)), - @"Playfield has non-proportional scaling. Flashlight implementations should be revisited with regard to balance."); - size *= Math.Abs(playfieldScale.X); - } - if (isBreakTime.Value) size *= 2.5f; else if (comboBasedSize) @@ -265,7 +254,11 @@ namespace osu.Game.Rulesets.Mods shader = Source.shader; screenSpaceDrawQuad = Source.ScreenSpaceDrawQuad; flashlightPosition = Vector2Extensions.Transform(Source.FlashlightPosition, DrawInfo.Matrix); - flashlightSize = Source.FlashlightSize * DrawInfo.Matrix.ExtractScale().Xy; + + // scale the flashlight based on the playfield to match gameplay components scale. + Vector2 drawInfoScale = Source.Playfield.DrawInfo.Matrix.ExtractScale().Xy; + flashlightSize = Source.FlashlightSize * drawInfoScale; + flashlightDim = Source.FlashlightDim; flashlightSmoothness = Source.flashlightSmoothness; } From 7530ad1a7b3ba6bfa9bd46383027aef19f372c5c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 20 Aug 2025 15:46:24 +0300 Subject: [PATCH 10/57] Adjust default flashlight size on osu! & osu!catch Because the flashlight is made to be scaled by playfield, there are constant scale factors applied somewhere in the `PlayfieldAdjustmentContainer` which needs to be reflected in the flashlight size to keep the size the same. The factor is specifically 1.6x, computed in {Osu,Catch}PlayfieldAdjustmentContainer.ScalingContainer`. More generally, I've deduced these factors by logging the difference between the `flashlightSize` before and after b78abe2f. --- osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs index 40450c6729..a5308f4cde 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Mods public override BindableBool ComboBasedSize { get; } = new BindableBool(true); - public override float DefaultFlashlightSize => 325; + public override float DefaultFlashlightSize => 203.125f; protected override Flashlight CreateFlashlight() => new CatchFlashlight(this, playfield); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index 3009530b50..a8c2508f80 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override BindableBool ComboBasedSize { get; } = new BindableBool(true); - public override float DefaultFlashlightSize => 200; + public override float DefaultFlashlightSize => 125; private OsuFlashlight flashlight = null!; From f374af7ce77a2fb77e52f2fbb88f3131eb560fa1 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 21 Aug 2025 17:15:39 +0300 Subject: [PATCH 11/57] Fix taiko flashlight applying aspect ratio twice --- .../Mods/TaikoModFlashlight.cs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs index 64f2f4c18a..02c06850b7 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs @@ -47,28 +47,15 @@ namespace osu.Game.Rulesets.Taiko.Mods { this.taikoPlayfield = taikoPlayfield; - FlashlightSize = adjustSizeForPlayfieldAspectRatio(GetSize()); + FlashlightSize = new Vector2(0, GetSize()); FlashlightSmoothness = 1.4f; AddLayout(flashlightProperties); } - /// - /// Returns the aspect ratio-adjusted size of the flashlight. - /// This ensures that the size of the flashlight remains independent of taiko-specific aspect ratio adjustments. - /// - /// - /// The size of the flashlight. - /// The value provided here should always come from . - /// - private Vector2 adjustSizeForPlayfieldAspectRatio(float size) - { - return new Vector2(0, size * taikoPlayfield.Parent!.Scale.Y); - } - protected override void UpdateFlashlightSize(float size) { - this.TransformTo(nameof(FlashlightSize), adjustSizeForPlayfieldAspectRatio(size), FLASHLIGHT_FADE_DURATION); + this.TransformTo(nameof(FlashlightSize), new Vector2(0, size), FLASHLIGHT_FADE_DURATION); } protected override string FragmentShader => "CircularFlashlight"; @@ -82,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Mods FlashlightPosition = ToLocalSpace(taikoPlayfield.HitTarget.ScreenSpaceDrawQuad.Centre); ClearTransforms(targetMember: nameof(FlashlightSize)); - FlashlightSize = adjustSizeForPlayfieldAspectRatio(GetSize()); + FlashlightSize = new Vector2(0, GetSize()); flashlightProperties.Validate(); } From 73624e4e25372b0c08a1ff0b4d0f1e059fc363c1 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 21 Aug 2025 17:16:06 +0300 Subject: [PATCH 12/57] Add visual test setup for taiko flashlight --- .../Mods/TestSceneTaikoModFlashlight.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModFlashlight.cs index 05a408c621..6dbd3478f1 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModFlashlight.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModFlashlight.cs @@ -3,7 +3,10 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics; using osu.Framework.Testing; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.UI; using osuTK; @@ -12,6 +15,34 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods { public partial class TestSceneTaikoModFlashlight : TaikoModTestScene { + [Test] + public void TestAspectRatios([Values] bool withClassicMod) + { + if (withClassicMod) + CreateModTest(new ModTestData { Mods = new Mod[] { new TaikoModFlashlight(), new TaikoModClassic() }, PassCondition = () => true }); + else + CreateModTest(new ModTestData { Mod = new TaikoModFlashlight(), PassCondition = () => true }); + + AddStep("clear dim", () => LocalConfig.SetValue(OsuSetting.DimLevel, 0.0)); + + AddStep("reset", () => Stack.FillMode = FillMode.Stretch); + AddStep("set to 16:9", () => + { + Stack.FillAspectRatio = 16 / 9f; + Stack.FillMode = FillMode.Fit; + }); + AddStep("set to 4:3", () => + { + Stack.FillAspectRatio = 4 / 3f; + Stack.FillMode = FillMode.Fit; + }); + AddSliderStep("aspect ratio", 0.01f, 5f, 1f, v => + { + Stack.FillAspectRatio = v; + Stack.FillMode = FillMode.Fit; + }); + } + [TestCase(1f)] [TestCase(0.5f)] [TestCase(1.25f)] From 0756c45d7073a53bd5b57090f6ba7390cdc81e63 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 22 Aug 2025 13:29:46 +0900 Subject: [PATCH 13/57] No longer download iOS simulator https://github.com/actions/runner-images/issues/12862#issuecomment-3209787203 --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 610648cfe4..7dfe3d11c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,9 +148,7 @@ jobs: # https://github.com/dotnet/macios/issues/19157 # https://github.com/actions/runner-images/issues/12758 - name: Use Xcode 16.4 - run: | - sudo xcode-select -switch /Applications/Xcode_16.4.app - xcodebuild -downloadPlatform iOS + run: sudo xcode-select -switch /Applications/Xcode_16.4.app - name: Build run: dotnet build -c Debug osu.iOS.slnf From d3ae20dd882381e109c20ca00ee5237e4dd1750d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Aug 2025 09:09:25 +0200 Subject: [PATCH 14/57] Pull up online beatmap set lookup to song select level to avoid two components doing the same fetch independently --- .../TestSceneBeatmapLeaderboardSorting.cs | 7 +- .../TestSceneBeatmapMetadataWedge.cs | 200 ++++++------------ .../TestSceneBeatmapTitleWedge.cs | 81 ++----- .../Screens/SelectV2/BeatmapMetadataWedge.cs | 47 +--- .../Screens/SelectV2/BeatmapTitleWedge.cs | 47 +--- osu.Game/Screens/SelectV2/SongSelect.cs | 76 ++++++- 6 files changed, 175 insertions(+), 283 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardSorting.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardSorting.cs index 0f66122bb5..6e3fafdd6a 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardSorting.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardSorting.cs @@ -7,6 +7,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -42,7 +43,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 private DialogOverlay dialogOverlay = null!; private LeaderboardManager leaderboardManager = null!; - private RealmPopulatingOnlineLookupSource lookupSource = null!; + + private readonly IBindable onlineLookupResult = new Bindable(); protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { @@ -52,7 +54,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API)); dependencies.Cache(leaderboardManager = new LeaderboardManager()); - dependencies.Cache(lookupSource = new RealmPopulatingOnlineLookupSource()); + dependencies.CacheAs(onlineLookupResult); Dependencies.Cache(Realm); @@ -68,7 +70,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); LoadComponent(leaderboardManager); - LoadComponent(lookupSource); Child = contentContainer = new OsuContextMenuContainer { diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs index ca52e476e2..d4fab55c62 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs @@ -4,13 +4,12 @@ using System; using System.Linq; using NUnit.Framework; -using osu.Framework.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; -using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Extensions; using osu.Game.Models; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens.SelectV2; @@ -18,64 +17,25 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { public partial class TestSceneBeatmapMetadataWedge : SongSelectComponentsTestScene { - private APIBeatmapSet? currentOnlineSet; - private BeatmapMetadataWedge wedge = null!; + [Cached(typeof(IBindable))] + private Bindable onlineLookupResult = new Bindable(); + protected override void LoadComplete() { base.LoadComplete(); - var lookupSource = new RealmPopulatingOnlineLookupSource(); - Child = new DependencyProvidingContainer + Child = wedge = new BeatmapMetadataWedge { - RelativeSizeAxes = Axes.Both, - CachedDependencies = [(typeof(RealmPopulatingOnlineLookupSource), lookupSource)], - Children = - [ - lookupSource, - wedge = new BeatmapMetadataWedge - { - State = { Value = Visibility.Visible }, - } - ] + State = { Value = Visibility.Visible }, }; } - [SetUpSteps] - public override void SetUpSteps() - { - AddStep("register request handling", () => - { - ((DummyAPIAccess)API).HandleRequest = request => - { - switch (request) - { - case GetBeatmapSetRequest set: - if (set.ID == currentOnlineSet?.OnlineID) - { - set.TriggerSuccess(currentOnlineSet); - return true; - } - - return false; - - default: - return false; - } - }; - }); - } - [Test] public void TestShowHide() { - AddStep("all metrics", () => - { - var (working, onlineSet) = createTestBeatmap(); - currentOnlineSet = onlineSet; - Beatmap.Value = working; - }); + AddStep("all metrics", () => (Beatmap.Value, onlineLookupResult.Value) = createTestBeatmap()); AddStep("hide wedge", () => wedge.Hide()); AddStep("show wedge", () => wedge.Show()); @@ -84,67 +44,63 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestVariousMetrics() { - AddStep("all metrics", () => - { - var (working, onlineSet) = createTestBeatmap(); - currentOnlineSet = onlineSet; - Beatmap.Value = working; - }); + AddStep("all metrics", () => (Beatmap.Value, onlineLookupResult.Value) = createTestBeatmap()); + AddStep("null beatmap", () => Beatmap.SetDefault()); AddStep("no source", () => { - var (working, onlineSet) = createTestBeatmap(); + var (working, online) = createTestBeatmap(); working.Metadata.Source = string.Empty; - currentOnlineSet = onlineSet; + onlineLookupResult.Value = online; Beatmap.Value = working; }); AddStep("no success rate", () => { - var (working, onlineSet) = createTestBeatmap(); + var (working, online) = createTestBeatmap(); - onlineSet.Beatmaps.Single().PlayCount = 0; - onlineSet.Beatmaps.Single().PassCount = 0; + online.Result!.Beatmaps.Single().PlayCount = 0; + online.Result!.Beatmaps.Single().PassCount = 0; - currentOnlineSet = onlineSet; + onlineLookupResult.Value = online; Beatmap.Value = working; }); AddStep("no user ratings", () => { - var (working, onlineSet) = createTestBeatmap(); + var (working, online) = createTestBeatmap(); - onlineSet.Ratings = Array.Empty(); + online.Result!.Ratings = Array.Empty(); - currentOnlineSet = onlineSet; + onlineLookupResult.Value = online; Beatmap.Value = working; }); AddStep("no fail times", () => { - var (working, onlineSet) = createTestBeatmap(); + var (working, online) = createTestBeatmap(); - onlineSet.Beatmaps.Single().FailTimes = null; + online.Result!.Beatmaps.Single().FailTimes = null; - currentOnlineSet = onlineSet; + onlineLookupResult.Value = online; Beatmap.Value = working; }); AddStep("no metrics", () => { - var (working, onlineSet) = createTestBeatmap(); + var (working, online) = createTestBeatmap(); - onlineSet.Ratings = Array.Empty(); - onlineSet.Beatmaps.Single().FailTimes = null; + online.Result!.Ratings = Array.Empty(); + online.Result!.Beatmaps.Single().FailTimes = null; - currentOnlineSet = onlineSet; + onlineLookupResult.Value = online; Beatmap.Value = working; }); AddStep("local beatmap", () => { - var (working, onlineSet) = createTestBeatmap(); + var (working, _) = createTestBeatmap(); working.BeatmapInfo.OnlineID = 0; - currentOnlineSet = onlineSet; + onlineLookupResult.Value = null; Beatmap.Value = working; }); } @@ -154,16 +110,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("long text", () => { - var (working, onlineSet) = createTestBeatmap(); + var (working, online) = createTestBeatmap(); working.BeatmapInfo.Metadata.Author = new RealmUser { Username = "Verrrrryyyy llooonngggggg author" }; working.BeatmapInfo.Metadata.Source = "Verrrrryyyy llooonngggggg source"; working.BeatmapInfo.Metadata.Tags = string.Join(' ', Enumerable.Repeat(working.BeatmapInfo.Metadata.Tags, 3)); - onlineSet.Genre = new BeatmapSetOnlineGenre { Id = 12, Name = "Verrrrryyyy llooonngggggg genre" }; - onlineSet.Language = new BeatmapSetOnlineLanguage { Id = 12, Name = "Verrrrryyyy llooonngggggg language" }; - onlineSet.Beatmaps.Single().TopTags = Enumerable.Repeat(onlineSet.Beatmaps.Single().TopTags, 3).SelectMany(t => t!).ToArray(); + online.Result!.Genre = new BeatmapSetOnlineGenre { Id = 12, Name = "Verrrrryyyy llooonngggggg genre" }; + online.Result!.Language = new BeatmapSetOnlineLanguage { Id = 12, Name = "Verrrrryyyy llooonngggggg language" }; + online.Result!.Beatmaps.Single().TopTags = Enumerable.Repeat(online.Result!.Beatmaps.Single().TopTags, 3).SelectMany(t => t!).ToArray(); - currentOnlineSet = onlineSet; + onlineLookupResult.Value = online; Beatmap.Value = working; }); } @@ -171,22 +127,17 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestOnlineAvailability() { - AddStep("online beatmapset", () => - { - var (working, onlineSet) = createTestBeatmap(); + AddStep("online beatmapset", () => (Beatmap.Value, onlineLookupResult.Value) = createTestBeatmap()); - currentOnlineSet = onlineSet; - Beatmap.Value = working; - }); AddUntilStep("rating wedge visible", () => wedge.RatingsVisible); AddUntilStep("fail time wedge visible", () => wedge.FailRetryVisible); AddStep("online beatmapset with local diff", () => { - var (working, onlineSet) = createTestBeatmap(); + var (working, lookupResult) = createTestBeatmap(); working.BeatmapInfo.ResetOnlineInfo(); - currentOnlineSet = onlineSet; + onlineLookupResult.Value = lookupResult; Beatmap.Value = working; }); AddUntilStep("rating wedge hidden", () => !wedge.RatingsVisible); @@ -195,7 +146,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { var (working, _) = createTestBeatmap(); - currentOnlineSet = null; + onlineLookupResult.Value = null; Beatmap.Value = working; }); AddAssert("rating wedge still hidden", () => !wedge.RatingsVisible); @@ -205,21 +156,17 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestUserTags() { - AddStep("user tags", () => - { - var (working, onlineSet) = createTestBeatmap(); + AddStep("user tags", () => (Beatmap.Value, onlineLookupResult.Value) = createTestBeatmap()); - currentOnlineSet = onlineSet; - Beatmap.Value = working; - }); AddStep("no user tags", () => { - var (working, onlineSet) = createTestBeatmap(); + var (working, online) = createTestBeatmap(); - onlineSet.Beatmaps.Single().TopTags = null; - onlineSet.RelatedTags = null; + online.Result!.Beatmaps.Single().TopTags = null; + online.Result!.RelatedTags = null; + working.BeatmapSetInfo.Beatmaps.Single().Metadata.UserTags.Clear(); - currentOnlineSet = onlineSet; + onlineLookupResult.Value = online; Beatmap.Value = working; }); } @@ -227,72 +174,60 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestLoading() { - AddStep("override request handling", () => - { - currentOnlineSet = null; - - ((DummyAPIAccess)API).HandleRequest = request => - { - switch (request) - { - case GetBeatmapSetRequest set: - Scheduler.AddDelayed(() => set.TriggerSuccess(currentOnlineSet!), 500); - return true; - - default: - return false; - } - }; - }); - AddStep("set beatmap", () => { - var (working, onlineSet) = createTestBeatmap(); + var (working, online) = createTestBeatmap(); - currentOnlineSet = onlineSet; + onlineLookupResult.Value = Screens.SelectV2.SongSelect.BeatmapSetLookupResult.InProgress(); + Scheduler.AddDelayed(() => onlineLookupResult.Value = online, 500); Beatmap.Value = working; }); AddWaitStep("wait", 5); AddStep("set beatmap", () => { - var (working, onlineSet) = createTestBeatmap(); + var (working, online) = createTestBeatmap(); - onlineSet.RelatedTags![0].Name = "other/tag"; - onlineSet.RelatedTags[1].Name = "another/tag"; - onlineSet.RelatedTags[2].Name = "some/tag"; + online.Result!.RelatedTags![0].Name = "other/tag"; + online.Result!.RelatedTags[1].Name = "another/tag"; + online.Result!.RelatedTags[2].Name = "some/tag"; - currentOnlineSet = onlineSet; + onlineLookupResult.Value = Screens.SelectV2.SongSelect.BeatmapSetLookupResult.InProgress(); + Scheduler.AddDelayed(() => onlineLookupResult.Value = online, 500); Beatmap.Value = working; }); AddWaitStep("wait", 5); AddStep("no user tags", () => { - var (working, onlineSet) = createTestBeatmap(); + var (working, online) = createTestBeatmap(); - onlineSet.Beatmaps.Single().TopTags = null; - onlineSet.RelatedTags = null; + online.Result!.Beatmaps.Single().TopTags = null; + online.Result!.RelatedTags = null; + working.BeatmapSetInfo.Beatmaps.Single().Metadata.UserTags.Clear(); - currentOnlineSet = onlineSet; + onlineLookupResult.Value = Screens.SelectV2.SongSelect.BeatmapSetLookupResult.InProgress(); + Scheduler.AddDelayed(() => onlineLookupResult.Value = online, 500); Beatmap.Value = working; }); AddWaitStep("wait", 5); AddStep("no user tags", () => { - var (working, onlineSet) = createTestBeatmap(); + var (working, online) = createTestBeatmap(); - onlineSet.Beatmaps.Single().TopTags = null; - onlineSet.RelatedTags = null; + online.Result!.Beatmaps.Single().TopTags = null; + online.Result!.RelatedTags = null; + working.BeatmapSetInfo.Beatmaps.Single().Metadata.UserTags.Clear(); - currentOnlineSet = onlineSet; + onlineLookupResult.Value = Screens.SelectV2.SongSelect.BeatmapSetLookupResult.InProgress(); + Scheduler.AddDelayed(() => onlineLookupResult.Value = online, 500); Beatmap.Value = working; }); AddWaitStep("wait", 5); } - private (WorkingBeatmap, APIBeatmapSet) createTestBeatmap() + private (WorkingBeatmap, Screens.SelectV2.SongSelect.BeatmapSetLookupResult) createTestBeatmap() { var working = CreateWorkingBeatmap(Ruleset.Value); var onlineSet = new APIBeatmapSet @@ -346,7 +281,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 working.BeatmapSetInfo.DateSubmitted = DateTimeOffset.Now; working.BeatmapSetInfo.DateRanked = DateTimeOffset.Now; - return (working, onlineSet); + working.Metadata.UserTags.AddRange(onlineSet.RelatedTags.Select(t => t.Name)); + return (working, Screens.SelectV2.SongSelect.BeatmapSetLookupResult.Completed(onlineSet)); } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs index cc4b38b54c..cbcf16ec51 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs @@ -11,6 +11,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; @@ -41,10 +42,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 private BeatmapTitleWedge titleWedge = null!; private BeatmapTitleWedge.DifficultyDisplay difficultyDisplay => titleWedge.ChildrenOfType().Single(); - private APIBeatmapSet? currentOnlineSet; - - [Cached] - private RealmPopulatingOnlineLookupSource lookupSource = new RealmPopulatingOnlineLookupSource(); + [Cached(typeof(IBindable))] + private Bindable onlineLookupResult = new Bindable(); [BackgroundDependencyLoader] private void load(RulesetStore rulesets) @@ -58,7 +57,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddRange(new Drawable[] { - lookupSource, new Container { RelativeSizeAxes = Axes.Both, @@ -142,44 +140,18 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestOnlineAvailability() { - AddStep("set up request handler", () => - { - ((DummyAPIAccess)API).HandleRequest = request => - { - switch (request) - { - case GetBeatmapSetRequest set: - if (set.ID == currentOnlineSet?.OnlineID) - { - set.TriggerSuccess(currentOnlineSet); - return true; - } + AddStep("online beatmapset", () => (Beatmap.Value, onlineLookupResult.Value) = createTestBeatmap()); - return false; - - default: - return false; - } - }; - }); - - AddStep("online beatmapset", () => - { - var (working, onlineSet) = createTestBeatmap(); - - currentOnlineSet = onlineSet; - Beatmap.Value = working; - }); AddUntilStep("play count is 10000", () => this.ChildrenOfType().ElementAt(0).Text.ToString(), () => Is.EqualTo("10,000")); AddUntilStep("favourites count is 2345", () => this.ChildrenOfType().Single().Text.ToString(), () => Is.EqualTo("2,345")); AddStep("online beatmapset with local diff", () => { - var (working, onlineSet) = createTestBeatmap(); + var (working, lookupResult) = createTestBeatmap(); working.BeatmapInfo.ResetOnlineInfo(); - currentOnlineSet = onlineSet; Beatmap.Value = working; + onlineLookupResult.Value = lookupResult; }); AddUntilStep("play count is -", () => this.ChildrenOfType().ElementAt(0).Text.ToString(), () => Is.EqualTo("-")); AddUntilStep("favourites count is 2345", () => this.ChildrenOfType().Single().Text.ToString(), () => Is.EqualTo("2,345")); @@ -187,8 +159,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { var (working, _) = createTestBeatmap(); - currentOnlineSet = null; Beatmap.Value = working; + onlineLookupResult.Value = Screens.SelectV2.SongSelect.BeatmapSetLookupResult.Completed(null); }); AddUntilStep("play count is -", () => this.ChildrenOfType().ElementAt(0).Text.ToString(), () => Is.EqualTo("-")); AddUntilStep("favourites count is -", () => this.ChildrenOfType().Single().Text.ToString(), () => Is.EqualTo("-")); @@ -205,15 +177,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { switch (request) { - case GetBeatmapSetRequest set: - if (set.ID == currentOnlineSet?.OnlineID) - { - set.TriggerSuccess(currentOnlineSet); - return true; - } - - return false; - case PostBeatmapFavouriteRequest favourite: Task.Run(() => { @@ -228,13 +191,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }; }); - AddStep("online beatmapset", () => - { - var (working, onlineSet) = createTestBeatmap(); + AddStep("online beatmapset", () => (Beatmap.Value, onlineLookupResult.Value) = createTestBeatmap()); - currentOnlineSet = onlineSet; - Beatmap.Value = working; - }); AddUntilStep("play count is 10000", () => this.ChildrenOfType().ElementAt(0).Text.ToString(), () => Is.EqualTo("10,000")); AddUntilStep("favourites count is 2345", () => this.ChildrenOfType().Single().Text.ToString(), () => Is.EqualTo("2,345")); @@ -251,13 +209,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("click favourite button", () => this.ChildrenOfType().Single().TriggerClick()); AddStep("change to another beatmap", () => { - var (working, onlineSet) = createTestBeatmap(); - onlineSet.FavouriteCount = 9999; - onlineSet.HasFavourited = true; - working.BeatmapSetInfo.OnlineID = onlineSet.OnlineID = 99999; + var (working, online) = createTestBeatmap(); + online.Result!.FavouriteCount = 9999; + online.Result!.HasFavourited = true; + working.BeatmapSetInfo.OnlineID = online.Result!.OnlineID = 99999; - currentOnlineSet = onlineSet; Beatmap.Value = working; + onlineLookupResult.Value = online; }); AddStep("allow request to complete", () => resetEvent.Set()); AddUntilStep("favourites count is 9999", () => this.ChildrenOfType().Single().Text.ToString(), () => Is.EqualTo("9,999")); @@ -268,15 +226,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { switch (request) { - case GetBeatmapSetRequest set: - if (set.ID == currentOnlineSet?.OnlineID) - { - set.TriggerSuccess(currentOnlineSet); - return true; - } - - return false; - case PostBeatmapFavouriteRequest favourite: Task.Run(() => { @@ -350,7 +299,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); } - private (WorkingBeatmap, APIBeatmapSet) createTestBeatmap() + private (WorkingBeatmap, Screens.SelectV2.SongSelect.BeatmapSetLookupResult) createTestBeatmap() { var working = CreateWorkingBeatmap(Ruleset.Value); var onlineSet = new APIBeatmapSet @@ -371,7 +320,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 working.BeatmapSetInfo.DateSubmitted = DateTimeOffset.Now; working.BeatmapSetInfo.DateRanked = DateTimeOffset.Now; - return (working, onlineSet); + return (working, Screens.SelectV2.SongSelect.BeatmapSetLookupResult.Completed(onlineSet)); } private class TestHitObject : ConvertHitObject; diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs index 37ac4cdb20..818176b3c4 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs @@ -3,16 +3,12 @@ using System; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; @@ -20,7 +16,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Localisation; using osu.Game.Online; using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Resources.Localisation.Web; using osuTK; @@ -55,10 +50,10 @@ namespace osu.Game.Screens.SelectV2 private IBindable beatmap { get; set; } = null!; [Resolved] - private IAPIProvider api { get; set; } = null!; + private IBindable onlineLookupResult { get; set; } = null!; [Resolved] - private RealmPopulatingOnlineLookupSource onlineLookupSource { get; set; } = null!; + private IAPIProvider api { get; set; } = null!; [Resolved] private RealmAccess realm { get; set; } = null!; @@ -254,6 +249,7 @@ namespace osu.Game.Screens.SelectV2 { base.LoadComplete(); beatmap.BindValueChanged(_ => updateDisplay()); + onlineLookupResult.BindValueChanged(_ => updateDisplay()); apiState = api.State.GetBoundCopy(); apiState.BindValueChanged(_ => Scheduler.AddOnce(updateDisplay), true); @@ -283,7 +279,7 @@ namespace osu.Game.Screens.SelectV2 // Needs some experimentation on what looks good. var beatmapInfo = beatmap.Value.BeatmapInfo; - var currentOnlineBeatmap = currentOnlineBeatmapSet?.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmapInfo.OnlineID); + var currentOnlineBeatmap = onlineLookupResult.Value?.Result?.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmapInfo.OnlineID); if (State.Value == Visibility.Visible && currentOnlineBeatmap != null) { @@ -365,41 +361,12 @@ namespace osu.Game.Screens.SelectV2 submitted.Date = beatmapSetInfo.DateSubmitted; ranked.Date = beatmapSetInfo.DateRanked; - if (currentOnlineBeatmapSet == null || currentOnlineBeatmapSet.OnlineID != beatmapSetInfo.OnlineID) - refetchBeatmapSet(); - updateOnlineDisplay(); } - private APIBeatmapSet? currentOnlineBeatmapSet; - private CancellationTokenSource? cancellationTokenSource; - private Task? currentFetchTask; - - private void refetchBeatmapSet() - { - var beatmapSetInfo = beatmap.Value.BeatmapSetInfo; - - cancellationTokenSource?.Cancel(); - currentOnlineBeatmapSet = null; - - if (beatmapSetInfo.OnlineID >= 1) - { - cancellationTokenSource = new CancellationTokenSource(); - currentFetchTask = onlineLookupSource.GetBeatmapSetAsync(beatmapSetInfo.OnlineID); - currentFetchTask.ContinueWith(t => - { - if (t.IsCompletedSuccessfully) - currentOnlineBeatmapSet = t.GetResultSafely(); - if (t.Exception != null) - Logger.Log($"Error when fetching online beatmap set: {t.Exception}", LoggingTarget.Network); - Scheduler.AddOnce(updateOnlineDisplay); - }); - } - } - private void updateOnlineDisplay() { - if (currentFetchTask?.IsCompleted == false) + if (onlineLookupResult.Value?.Status != SongSelect.BeatmapSetLookupStatus.Completed) { genre.Data = null; language.Data = null; @@ -407,7 +374,7 @@ namespace osu.Game.Screens.SelectV2 return; } - if (currentOnlineBeatmapSet == null) + if (onlineLookupResult.Value.Result == null) { genre.Data = ("-", null); language.Data = ("-", null); @@ -416,7 +383,7 @@ namespace osu.Game.Screens.SelectV2 { var beatmapInfo = beatmap.Value.BeatmapInfo; - var onlineBeatmapSet = currentOnlineBeatmapSet; + var onlineBeatmapSet = onlineLookupResult.Value.Result; var onlineBeatmap = onlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmapInfo.OnlineID); genre.Data = (onlineBeatmapSet.Genre.Name, () => songSelect?.Search(onlineBeatmapSet.Genre.Name)); diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs index 157e2c2896..21ac04b18a 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs @@ -8,11 +8,9 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; -using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Configuration; @@ -21,7 +19,6 @@ using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -43,6 +40,9 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IBindable> mods { get; set; } = null!; + [Resolved] + private IBindable onlineLookupResult { get; set; } = null!; + protected override bool StartHidden => true; private ModSettingChangeTracker? settingChangeTracker; @@ -69,16 +69,9 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private LocalisationManager localisation { get; set; } = null!; - [Resolved] - private RealmPopulatingOnlineLookupSource onlineLookupSource { get; set; } = null!; - [Resolved] private RealmAccess realm { get; set; } = null!; - private APIBeatmapSet? currentOnlineBeatmapSet; - private CancellationTokenSource? cancellationTokenSource; - private Task? currentFetchTask; - private FillFlowContainer statisticsFlow = null!; public BeatmapTitleWedge() @@ -190,6 +183,7 @@ namespace osu.Game.Screens.SelectV2 working.BindValueChanged(_ => updateDisplay()); ruleset.BindValueChanged(_ => updateDisplay()); + onlineLookupResult.BindValueChanged(_ => updateDisplay()); mods.BindValueChanged(m => { @@ -230,7 +224,6 @@ namespace osu.Game.Screens.SelectV2 { var metadata = working.Value.Metadata; var beatmapInfo = working.Value.BeatmapInfo; - var beatmapSetInfo = working.Value.BeatmapSetInfo; statusPill.Status = beatmapInfo.Status; @@ -243,10 +236,6 @@ namespace osu.Game.Screens.SelectV2 artistLink.Action = () => songSelect?.Search(artistText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)); updateLengthAndBpmStatistics(); - - if (currentOnlineBeatmapSet == null || currentOnlineBeatmapSet.OnlineID != beatmapSetInfo.OnlineID) - refetchBeatmapSet(); - updateOnlineDisplay(); } @@ -289,40 +278,18 @@ namespace osu.Game.Screens.SelectV2 }, token); } - private void refetchBeatmapSet() - { - var beatmapSetInfo = working.Value.BeatmapSetInfo; - - cancellationTokenSource?.Cancel(); - currentOnlineBeatmapSet = null; - - if (beatmapSetInfo.OnlineID >= 1) - { - cancellationTokenSource = new CancellationTokenSource(); - currentFetchTask = onlineLookupSource.GetBeatmapSetAsync(beatmapSetInfo.OnlineID); - currentFetchTask.ContinueWith(t => - { - if (t.IsCompletedSuccessfully) - currentOnlineBeatmapSet = t.GetResultSafely(); - if (t.Exception != null) - Logger.Log($"Error when fetching online beatmap set: {t.Exception}", LoggingTarget.Network); - Scheduler.AddOnce(updateOnlineDisplay); - }); - } - } - private void updateOnlineDisplay() { - if (currentFetchTask?.IsCompleted == false) + if (onlineLookupResult.Value?.Status != SongSelect.BeatmapSetLookupStatus.Completed) { playCount.Value = null; favouriteButton.SetLoading(); } else { - var onlineBeatmap = currentOnlineBeatmapSet?.Beatmaps.SingleOrDefault(b => b.OnlineID == working.Value.BeatmapInfo.OnlineID); + var onlineBeatmap = onlineLookupResult.Value.Result?.Beatmaps.SingleOrDefault(b => b.OnlineID == working.Value.BeatmapInfo.OnlineID); playCount.Value = new StatisticPlayCount.Data(onlineBeatmap?.PlayCount ?? -1, onlineBeatmap?.UserPlayCount ?? -1); - favouriteButton.SetBeatmapSet(currentOnlineBeatmapSet); + favouriteButton.SetBeatmapSet(onlineLookupResult.Value.Result); // the online fetch may have also updated the beatmap's status. // this needs to be checked against the *local* beatmap model rather than the online one, because it's not known here whether the status change has occurred or not diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 4ff571a3f8..947b8f9c7c 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -5,11 +5,14 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -34,6 +37,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Volume; @@ -133,8 +137,7 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IDialogOverlay? dialogOverlay { get; set; } - [Cached] - private RealmPopulatingOnlineLookupSource onlineLookupSource = new RealmPopulatingOnlineLookupSource(); + private readonly RealmPopulatingOnlineLookupSource onlineLookupSource = new RealmPopulatingOnlineLookupSource(); private Bindable configBackgroundBlur = null!; @@ -349,6 +352,7 @@ namespace osu.Game.Screens.SelectV2 ensurePlayingSelected(); updateBackgroundDim(); updateWedgeVisibility(); + fetchOnlineInfo(); }); } @@ -954,6 +958,74 @@ namespace osu.Game.Screens.SelectV2 #endregion + #region Online lookups + + public enum BeatmapSetLookupStatus + { + InProgress, + Completed, + } + + public class BeatmapSetLookupResult + { + public BeatmapSetLookupStatus Status { get; } + public APIBeatmapSet? Result { get; } + + private BeatmapSetLookupResult(BeatmapSetLookupStatus status, APIBeatmapSet? result) + { + Status = status; + Result = result; + } + + public static BeatmapSetLookupResult InProgress() => new BeatmapSetLookupResult(BeatmapSetLookupStatus.InProgress, null); + public static BeatmapSetLookupResult Completed(APIBeatmapSet? beatmapSet) => new BeatmapSetLookupResult(BeatmapSetLookupStatus.Completed, beatmapSet); + } + + /// + /// Result of the latest online beatmap set lookup. + /// Note that this being or is different from + /// being a with a of null. + /// The former indicates a lookup never occurring or being in progress, while the latter indicates a completed lookup with no result. + /// + [Cached(typeof(IBindable))] + private readonly Bindable lastLookupResult = new Bindable(); + + private CancellationTokenSource? onlineLookupCancellation; + private Task? currentOnlineLookup; + + private void fetchOnlineInfo() + { + var beatmapSetInfo = Beatmap.Value.BeatmapSetInfo; + + if (lastLookupResult.Value?.Result?.OnlineID == beatmapSetInfo.OnlineID) + return; + + onlineLookupCancellation?.Cancel(); + + if (beatmapSetInfo.OnlineID < 0) + { + lastLookupResult.Value = BeatmapSetLookupResult.Completed(null); + return; + } + + lastLookupResult.Value = BeatmapSetLookupResult.InProgress(); + onlineLookupCancellation = new CancellationTokenSource(); + currentOnlineLookup = onlineLookupSource.GetBeatmapSetAsync(beatmapSetInfo.OnlineID); + currentOnlineLookup.ContinueWith(t => + { + if (t.IsCompletedSuccessfully) + Schedule(() => lastLookupResult.Value = BeatmapSetLookupResult.Completed(t.GetResultSafely())); + + if (t.Exception != null) + { + Logger.Log($"Error when fetching online beatmap set: {t.Exception}", LoggingTarget.Network); + Schedule(() => lastLookupResult.Value = BeatmapSetLookupResult.Completed(null)); + } + }); + } + + #endregion + #region Implementation of ISongSelect void ISongSelect.Search(string query) => filterControl.Search(query); From 5292d4a04e892c2d19aa26e13d7a00ed17371060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Aug 2025 09:51:47 +0200 Subject: [PATCH 15/57] Fix song select favourite button potentially showing stale data from (un)favourite request callback --- .../SelectV2/BeatmapTitleWedge_FavouriteButton.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_FavouriteButton.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_FavouriteButton.cs index 39ef0822d7..2db3ed7613 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_FavouriteButton.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_FavouriteButton.cs @@ -229,7 +229,10 @@ namespace osu.Game.Screens.SelectV2 bool hasFavourited = favouriteRequest.Action == BeatmapFavouriteAction.Favourite; beatmapSet.HasFavourited = hasFavourited; beatmapSet.FavouriteCount += hasFavourited ? 1 : -1; - setBeatmapSet(beatmapSet, withHeartAnimation: hasFavourited); + + // if the beatmap set reference changed under the callback, abort visual updates to avoid showing stale data + if (onlineBeatmapSet == null || ReferenceEquals(beatmapSet, onlineBeatmapSet)) + setBeatmapSet(beatmapSet, withHeartAnimation: hasFavourited); }; favouriteRequest.Failure += e => { @@ -238,7 +241,10 @@ namespace osu.Game.Screens.SelectV2 Text = e.Message, Icon = FontAwesome.Solid.Times, }); - setBeatmapSet(beatmapSet, withHeartAnimation: false); + + // if the beatmap set reference changed under the callback, abort visual updates to avoid showing stale data + if (onlineBeatmapSet == null || ReferenceEquals(beatmapSet, onlineBeatmapSet)) + setBeatmapSet(beatmapSet, withHeartAnimation: false); }; api.Queue(favouriteRequest); setLoading(); From c0c36909083cd9dfcc106d7936637c37331c2867 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 23 Aug 2025 09:28:14 +0300 Subject: [PATCH 16/57] Remove no longer valid test --- .../Mods/TestSceneOsuModFlashlight.cs | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs index 496e7610ff..f4f7f9d44b 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs @@ -32,26 +32,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods [Test] public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true }); - [Test] - public void TestPlayfieldBasedSize() - { - OsuModFlashlight flashlight; - CreateModTest(new ModTestData - { - Mods = [flashlight = new OsuModFlashlight(), new OsuModBarrelRoll()], - PassCondition = () => - { - var flashlightOverlay = Player.DrawableRuleset.Overlays - .ChildrenOfType.Flashlight>() - .First(); - - // the combo check is here because the flashlight radius decreases for the first time at 100 combo - // and hardcoding it here eliminates the need to meddle in flashlight internals further by e.g. exposing `GetComboScaleFor()` - return flashlightOverlay.GetSize() < flashlight.DefaultFlashlightSize && Player.GameplayState.ScoreProcessor.Combo.Value < 100; - } - }); - } - [Test] public void TestSliderDimsOnlyAfterStartTime() { From bc59270f3ef7132570442382ad840862b123e653 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Aug 2025 17:50:25 +0300 Subject: [PATCH 17/57] Fix flashlight not handling internal playfield sizing changes Note that this does not handle sizing/scaling changes applied directly to `Playfield`, but it handles any changes within the layers inside `PlayfieldAdjustmentContainer`. --- osu.Game/Rulesets/Mods/ModFlashlight.cs | 42 +++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index 6b24e8b77f..0295fea84e 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Shaders.Types; using osu.Framework.Graphics.Sprites; +using osu.Framework.Layout; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics; @@ -83,7 +84,11 @@ namespace osu.Game.Rulesets.Mods flashlight.Colour = Color4.Black; flashlight.Combo.BindTo(Combo); - flashlight.Playfield = drawableRuleset.Playfield; + + var playfieldDrawInfoTracker = new PlayfieldDrawInfoTracker(); + + drawableRuleset.PlayfieldAdjustmentContainer.Add(playfieldDrawInfoTracker); + flashlight.PlayfieldDrawInfoTracker = playfieldDrawInfoTracker; drawableRuleset.Overlays.Add(new Container { @@ -109,7 +114,9 @@ namespace osu.Game.Rulesets.Mods public override bool RemoveCompletedTransforms => false; - internal Playfield Playfield { get; set; } = null!; + internal PlayfieldDrawInfoTracker PlayfieldDrawInfoTracker { get; set; } = null!; + + private DrawInfo playfieldDrawInfo => PlayfieldDrawInfoTracker.DrawInfo; private readonly float defaultFlashlightSize; private readonly float sizeMultiplier; @@ -144,6 +151,8 @@ namespace osu.Game.Rulesets.Mods isBreakTime.BindTo(player.IsBreakTime); isBreakTime.BindValueChanged(_ => UpdateFlashlightSize(GetSize()), true); } + + PlayfieldDrawInfoTracker.OnDrawInfoInvalidate += () => Invalidate(Invalidation.DrawNode); } protected abstract void UpdateFlashlightSize(float size); @@ -256,7 +265,7 @@ namespace osu.Game.Rulesets.Mods flashlightPosition = Vector2Extensions.Transform(Source.FlashlightPosition, DrawInfo.Matrix); // scale the flashlight based on the playfield to match gameplay components scale. - Vector2 drawInfoScale = Source.Playfield.DrawInfo.Matrix.ExtractScale().Xy; + Vector2 drawInfoScale = Source.playfieldDrawInfo.Matrix.ExtractScale().Xy; flashlightSize = Source.FlashlightSize * drawInfoScale; flashlightDim = Source.FlashlightDim; @@ -314,5 +323,32 @@ namespace osu.Game.Rulesets.Mods } } } + + /// + /// The purpose of this component is to track any changes to (technically its parent), + /// in order for the flashlight to invalidate its draw node and read any changes in the playfield's scaling. + /// + internal partial class PlayfieldDrawInfoTracker : Component + { + private readonly LayoutValue drawInfoLayout = new LayoutValue(Invalidation.DrawInfo); + + public Action? OnDrawInfoInvalidate; + + public PlayfieldDrawInfoTracker() + { + AddLayout(drawInfoLayout); + } + + protected override void Update() + { + base.Update(); + + if (!drawInfoLayout.IsValid) + { + OnDrawInfoInvalidate?.Invoke(); + drawInfoLayout.Validate(); + } + } + } } } From 3cca458c21fb2e232932e8345cc2519a202dd243 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Aug 2025 18:55:40 +0300 Subject: [PATCH 18/57] Fix xmldoc error and reword --- osu.Game/Rulesets/Mods/ModFlashlight.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index 0295fea84e..884066d7ab 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -325,8 +325,9 @@ namespace osu.Game.Rulesets.Mods } /// - /// The purpose of this component is to track any changes to (technically its parent), - /// in order for the flashlight to invalidate its draw node and read any changes in the playfield's scaling. + /// The purpose of this component is to track any changes to Playfield.Parent.DrawInfo + /// (by being added to the content of ). + /// All in order for the flashlight to invalidate its draw node and read any changes in the playfield's scaling. /// internal partial class PlayfieldDrawInfoTracker : Component { From a2bf8e398807a45fba99a14f03b063a48a5192f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Aug 2025 13:43:03 +0200 Subject: [PATCH 19/57] Fix copy-paste fail in log message --- osu.Game/Database/BackgroundDataStoreProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index c0f2238219..f8d1b9ae51 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -727,7 +727,7 @@ namespace osu.Game.Database } catch (Exception e) { - Logger.Log(@$"Failed to update ranked/submitted dates for beatmap set {id}: {e}"); + Logger.Log(@$"Failed to update user tags for beatmap {id}: {e}"); ++failedCount; } } From e908b80359bddde15cc0ca87fedb6bb512d5f065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Aug 2025 14:20:05 +0200 Subject: [PATCH 20/57] Fix aim error meter applying incorrect scaling constant in relative mode Closes https://github.com/ppy/osu/issues/34769 Visible (and easiest to check) in test scene. --- osu.Game.Rulesets.Osu/HUD/AimErrorMeter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/HUD/AimErrorMeter.cs b/osu.Game.Rulesets.Osu/HUD/AimErrorMeter.cs index 8b3d505439..46593a56bb 100644 --- a/osu.Game.Rulesets.Osu/HUD/AimErrorMeter.cs +++ b/osu.Game.Rulesets.Osu/HUD/AimErrorMeter.cs @@ -323,7 +323,7 @@ namespace osu.Game.Rulesets.Osu.HUD if (PositionDisplayStyle.Value == PositionDisplay.Normalised && lastObjectPosition != null) { hitPosition = AccuracyHeatmap.FindRelativeHitPosition(lastObjectPosition.Value, ((OsuHitObject)circleJudgement.HitObject).StackedEndPosition, - circleJudgement.CursorPositionAtHit.Value, objectRadius, 45) * 0.5f; + circleJudgement.CursorPositionAtHit.Value, objectRadius, 45) * (inner_portion / 2); } else { From 196b28115ebf327d9b24edc32eb6ba899cfd8b8d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Aug 2025 17:51:02 +0900 Subject: [PATCH 21/57] Fix playlist leaderboard provider potentially inserting local user in wrong order Due to `Perform` being used from a BDL method in conjunction with `Success` (which is scheduled to the *update* thread), there was a chance that the order of execution would be not quite as intended. To rectify, let's not use `Success` and just continue with synchronous flow. --- .../Leaderboards/PlaylistsGameplayLeaderboardProvider.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/Leaderboards/PlaylistsGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/PlaylistsGameplayLeaderboardProvider.cs index c60e06939b..3044e1a0e2 100644 --- a/osu.Game/Screens/Select/Leaderboards/PlaylistsGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/PlaylistsGameplayLeaderboardProvider.cs @@ -37,7 +37,11 @@ namespace osu.Game.Screens.Select.Leaderboards var scoresToShow = new List(); var scoresRequest = new IndexPlaylistScoresRequest(room.RoomID!.Value, playlistItem.ID); - scoresRequest.Success += response => + api.Perform(scoresRequest); + + var response = scoresRequest.Response; + + if (response != null) { isPartial = response.Scores.Count < response.TotalScores; @@ -50,8 +54,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (response.UserScore != null && response.Scores.All(s => s.ID != response.UserScore.ID)) scoresToShow.Add(new GameplayLeaderboardScore(response.UserScore, tracked: false, GameplayLeaderboardScore.ComboDisplayMode.Highest)); - }; - api.Perform(scoresRequest); + } if (gameplayState != null) { From 3f179e390320df97ffd35581e1e2f6ccee1205cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Aug 2025 17:51:14 +0900 Subject: [PATCH 22/57] Sort scores immediately for good measure --- .../Leaderboards/PlaylistsGameplayLeaderboardProvider.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Leaderboards/PlaylistsGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/PlaylistsGameplayLeaderboardProvider.cs index 3044e1a0e2..ea0a2b68dc 100644 --- a/osu.Game/Screens/Select/Leaderboards/PlaylistsGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/PlaylistsGameplayLeaderboardProvider.cs @@ -65,8 +65,12 @@ namespace osu.Game.Screens.Select.Leaderboards // touching the public bindable must happen on the update thread for general thread safety, // since we may have external subscribers bound already - Schedule(() => scores.AddRange(scoresToShow)); - Scheduler.AddDelayed(sort, 1000, true); + Schedule(() => + { + scores.AddRange(scoresToShow); + sort(); + Scheduler.AddDelayed(sort, 1000, true); + }); } // logic shared with SoloGameplayLeaderboardProvider From 4bafbfb9e49592685dab723c5f4e47b25361ff47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Aug 2025 18:30:12 +0900 Subject: [PATCH 23/57] Apply NRT to `ReplayPlayer` for good measure --- osu.Game/Screens/Play/ReplayPlayer.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 131ce452bc..83295f82d7 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -31,17 +29,17 @@ namespace osu.Game.Screens.Play private readonly Func, Score> createScore; - private PlaybackSettings playbackSettings; + private PlaybackSettings playbackSettings = null!; [Cached(typeof(IGameplayLeaderboardProvider))] private readonly SoloGameplayLeaderboardProvider leaderboardProvider = new SoloGameplayLeaderboardProvider(); - protected override UserActivity InitialActivity => new UserActivity.WatchingReplay(Score.ScoreInfo); + protected override UserActivity? InitialActivity => new UserActivity.WatchingReplay(Score.ScoreInfo); private bool isAutoplayPlayback => GameplayState.Mods.OfType().Any(); private double? lastFrameTime; - private ReplayFailIndicator failIndicator; + private ReplayFailIndicator failIndicator = null!; protected override bool CheckModsAllowFailure() { @@ -60,12 +58,12 @@ namespace osu.Game.Screens.Play return false; } - public ReplayPlayer(Score score, PlayerConfiguration configuration = null) + public ReplayPlayer(Score score, PlayerConfiguration? configuration = null) : this((_, _) => score, configuration) { } - public ReplayPlayer(Func, Score> createScore, PlayerConfiguration configuration = null) + public ReplayPlayer(Func, Score> createScore, PlayerConfiguration? configuration = null) : base(configuration) { this.createScore = createScore; From 4d851f252782c819532e366fa1addab4846ae25a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Aug 2025 18:31:39 +0900 Subject: [PATCH 24/57] Fix crash on exiting `ReplayPlayer` is beatmap was not loaded successfully Closes https://github.com/ppy/osu/issues/34763. --- osu.Game/Screens/Play/ReplayPlayer.cs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 83295f82d7..51cfb9a9f3 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -29,8 +29,6 @@ namespace osu.Game.Screens.Play private readonly Func, Score> createScore; - private PlaybackSettings playbackSettings = null!; - [Cached(typeof(IGameplayLeaderboardProvider))] private readonly SoloGameplayLeaderboardProvider leaderboardProvider = new SoloGameplayLeaderboardProvider(); @@ -39,7 +37,9 @@ namespace osu.Game.Screens.Play private bool isAutoplayPlayback => GameplayState.Mods.OfType().Any(); private double? lastFrameTime; - private ReplayFailIndicator failIndicator = null!; + + private ReplayFailIndicator? failIndicator; + private PlaybackSettings? playbackSettings; protected override bool CheckModsAllowFailure() { @@ -131,6 +131,9 @@ namespace osu.Game.Screens.Play public bool OnPressed(KeyBindingPressEvent e) { + if (!LoadedBeatmapSuccessfully) + return false; + switch (e.Action) { case GlobalAction.StepReplayBackward: @@ -142,11 +145,11 @@ namespace osu.Game.Screens.Play return true; case GlobalAction.SeekReplayBackward: - SeekInDirection(-5 * (float)playbackSettings.UserPlaybackRate.Value); + SeekInDirection(-5 * (float)playbackSettings!.UserPlaybackRate.Value); return true; case GlobalAction.SeekReplayForward: - SeekInDirection(5 * (float)playbackSettings.UserPlaybackRate.Value); + SeekInDirection(5 * (float)playbackSettings!.UserPlaybackRate.Value); return true; case GlobalAction.TogglePauseReplay: @@ -190,7 +193,7 @@ namespace osu.Game.Screens.Play { // base logic intentionally suppressed - we have our own custom fail interaction ScoreProcessor.FailScore(Score.ScoreInfo); - failIndicator.Display(); + failIndicator!.Display(); } public override void OnSuspending(ScreenTransitionEvent e) @@ -202,18 +205,18 @@ namespace osu.Game.Screens.Play public override bool OnExiting(ScreenExitEvent e) { // safety against filters or samples from the indicator playing long after the screen is exited - failIndicator.RemoveAndDisposeImmediately(); + failIndicator?.RemoveAndDisposeImmediately(); return base.OnExiting(e); } private void stopAllAudioEffects() { // safety against filters or samples from the indicator playing long after the screen is exited - failIndicator.RemoveAndDisposeImmediately(); + failIndicator?.RemoveAndDisposeImmediately(); if (GameplayClockContainer is MasterGameplayClockContainer master) { - playbackSettings.UserPlaybackRate.UnbindFrom(master.UserPlaybackRate); + playbackSettings?.UserPlaybackRate.UnbindFrom(master.UserPlaybackRate); master.UserPlaybackRate.SetDefault(); } } From 2ccb65aa653f57e23f68a6debf3f953db0f53427 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Aug 2025 18:40:52 +0900 Subject: [PATCH 25/57] Add test coverage and fix one more fail case --- osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs | 8 ++++++++ osu.Game/Screens/Play/ReplayPlayer.cs | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs index 4a0f5fec6c..6be8f7d185 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs @@ -24,6 +24,14 @@ namespace osu.Game.Tests.Visual.Gameplay { protected TestReplayPlayer Player = null!; + [Test] + public void TestFailedBeatmapLoad() + { + loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo, withHitObjects: false)); + + AddUntilStep("wait for exit", () => Player.IsCurrentScreen()); + } + [Test] public void TestPauseViaSpace() { diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 51cfb9a9f3..1c583609d9 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -32,7 +32,9 @@ namespace osu.Game.Screens.Play [Cached(typeof(IGameplayLeaderboardProvider))] private readonly SoloGameplayLeaderboardProvider leaderboardProvider = new SoloGameplayLeaderboardProvider(); - protected override UserActivity? InitialActivity => new UserActivity.WatchingReplay(Score.ScoreInfo); + protected override UserActivity? InitialActivity => + // score may be null if LoadedBeatmapSuccessfully is false. + Score == null ? null : new UserActivity.WatchingReplay(Score.ScoreInfo); private bool isAutoplayPlayback => GameplayState.Mods.OfType().Any(); From c0fd5637de04b7f79715cb8a29180c14ae7305af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Aug 2025 13:27:54 +0200 Subject: [PATCH 26/57] Work around excessive refreshes of carousel beatmap set panel backgrounds Closes https://github.com/ppy/osu/issues/34511 I guess. --- .../Screens/SelectV2/PanelSetBackground.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/PanelSetBackground.cs b/osu.Game/Screens/SelectV2/PanelSetBackground.cs index d6221fa395..1b49f48ea6 100644 --- a/osu.Game/Screens/SelectV2/PanelSetBackground.cs +++ b/osu.Game/Screens/SelectV2/PanelSetBackground.cs @@ -37,7 +37,21 @@ namespace osu.Game.Screens.SelectV2 get => working; set { - if (value == working) + if (working == null && value == null) + return; + + // this guard papers over excessive refreshes of the background asset which occur if `working == value` type guards are used. + // the root cause of why `working == value` type guards fail here is that `SongSelect` will invalidate working beatmaps very often + // (via https://github.com/ppy/osu/blob/d3ae20dd882381e109c20ca00ee5237e4dd1750d/osu.Game/Screens/SelectV2/SongSelect.cs#L506-L507), + // due to a variety of causes, ranging from "someone typed a letter in the search box" (which triggers a refilter -> presentation of new items -> `ensureGlobalBeatmapValid()`), + // to "someone just went into the editor and replaced every single file in the set, including the background". + // the following guard approximates the most appropriate debounce criterion, which is the contents of the actual asset that is supposed to be displayed in the background, + // i.e. if the hash of the new background file matches the old, then we do not bother updating the working beatmap here. + // + // note that this is basically a reimplementation of the caching scheme in `WorkingBeatmapCache.getBackgroundFromStore()`, + // which cannot be used directly by retrieving the texture and checking texture reference equality, + // because missing the cache would incur a synchronous texture load on the update thread. + if (getBackgroundFileHash(working) == getBackgroundFileHash(value)) return; working = value; @@ -52,6 +66,9 @@ namespace osu.Game.Screens.SelectV2 } } + private static string? getBackgroundFileHash(WorkingBeatmap? working) + => working?.BeatmapSetInfo.GetFile(working.Metadata.BackgroundFile)?.File.Hash; + public PanelSetBackground() { RelativeSizeAxes = Axes.Both; From 68677200f3a758379fdefd01655bdf035504ecca Mon Sep 17 00:00:00 2001 From: Binwalker Date: Sat, 23 Aug 2025 16:43:36 +0900 Subject: [PATCH 27/57] feat(ManiaFilterCriteria): add long note ratio filter for mania --- .../ManiaFilterCriteria.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index 9b2700c6e8..3b5736ad9f 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.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.Linq; using osu.Framework.Bindables; @@ -19,12 +20,16 @@ namespace osu.Game.Rulesets.Mania public class ManiaFilterCriteria : IRulesetFilterCriteria { private readonly HashSet includedKeyCounts = Enumerable.Range(1, LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT).ToHashSet(); + private FilterCriteria.OptionalRange longNoteRatio; public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) { int keyCount = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods); - return includedKeyCounts.Contains(keyCount); + bool keyCountMatch = includedKeyCounts.Contains(keyCount); + bool longNoteRatioMatch = !longNoteRatio.HasFilter || longNoteRatio.IsInRange(calculatelongNoteRatio(beatmapInfo)); + + return keyCountMatch && longNoteRatioMatch; } public bool TryParseCustomKeywordCriteria(string key, Operator op, string strValues) @@ -84,11 +89,24 @@ namespace osu.Game.Rulesets.Mania return false; } } + + case "ln": + case "lns": + return FilterQueryParser.TryUpdateCriteriaRange(ref longNoteRatio, op, strValues); } return false; } + private static float calculatelongNoteRatio(BeatmapInfo beatmapInfo) + { + int holdNotes = beatmapInfo.EndTimeObjectCount; + int totalNotes = beatmapInfo.TotalObjectCount; + int sum = Math.Max(1, totalNotes); + + return holdNotes / (float)sum * 100; + } + public bool FilterMayChangeFromMods(ValueChangedEvent> mods) { if (includedKeyCounts.Count != LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT) From f7b0e114a9f33e1bedd30508d5ce9bac455ed04c Mon Sep 17 00:00:00 2001 From: Binwalker Date: Sat, 23 Aug 2025 16:43:46 +0900 Subject: [PATCH 28/57] test(ManiaFilterCriteriaTest): add some testcase --- .../ManiaFilterCriteriaTest.cs | 224 +++++++++++++++++- 1 file changed, 223 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs index 24da447482..a7686c7320 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs @@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Mania.Tests } [TestCase] - public void TestInvalidFilters() + public void TestInvalidKeysFilters() { var criteria = new ManiaFilterCriteria(); @@ -183,5 +183,227 @@ namespace osu.Game.Rulesets.Mania.Tests Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "4,some text")); Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4,5,6")); } + + [TestCase] + public void TestLnsEqualSingleValue() + { + var criteria = new ManiaFilterCriteria(); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "50"); + BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 50 + }; + Assert.True(criteria.Matches(beatmapInfo1, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0"); + BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 0, + EndTimeObjectCount = 0 + }; + Assert.True(criteria.Matches(beatmapInfo2, new FilterCriteria())); + + BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 0 + }; + Assert.True(criteria.Matches(beatmapInfo3, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "1"); + BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 1 + }; + Assert.True(criteria.Matches(beatmapInfo4, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "100"); + BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 100 + }; + Assert.True(criteria.Matches(beatmapInfo5, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0.1"); + BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 1000, + EndTimeObjectCount = 1 + }; + Assert.True(criteria.Matches(beatmapInfo6, new FilterCriteria())); + } + + [TestCase] + public void TestLnsNotEqual() + { + var criteria = new ManiaFilterCriteria(); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "50"); + BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 50 + }; + Assert.False(criteria.Matches(beatmapInfo1, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "0"); + BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 0, + EndTimeObjectCount = 0 + }; + Assert.False(criteria.Matches(beatmapInfo2, new FilterCriteria())); + + BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 0 + }; + Assert.False(criteria.Matches(beatmapInfo3, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "1"); + BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 1 + }; + Assert.False(criteria.Matches(beatmapInfo4, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "100"); + BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 100 + }; + Assert.False(criteria.Matches(beatmapInfo5, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "0.1"); + BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 1000, + EndTimeObjectCount = 1 + }; + Assert.False(criteria.Matches(beatmapInfo6, new FilterCriteria())); + } + + [TestCase] + public void TestLnsGreaterOrEqual() + { + var criteria = new ManiaFilterCriteria(); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "50"); + BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 50 + }; + Assert.True(criteria.Matches(beatmapInfo1, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0"); + BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 0, + EndTimeObjectCount = 0 + }; + Assert.True(criteria.Matches(beatmapInfo2, new FilterCriteria())); + + BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 0 + }; + Assert.True(criteria.Matches(beatmapInfo3, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1"); + BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 1 + }; + Assert.True(criteria.Matches(beatmapInfo4, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "100"); + BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 100 + }; + Assert.True(criteria.Matches(beatmapInfo5, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0.1"); + BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 1000, + EndTimeObjectCount = 1 + }; + Assert.True(criteria.Matches(beatmapInfo6, new FilterCriteria())); + } + + [TestCase] + public void TestLnsGreater() + { + var criteria = new ManiaFilterCriteria(); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "49"); + BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 50 + }; + Assert.True(criteria.Matches(beatmapInfo1, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0"); + BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 0, + EndTimeObjectCount = 0 + }; + Assert.False(criteria.Matches(beatmapInfo2, new FilterCriteria())); + + BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 0 + }; + Assert.False(criteria.Matches(beatmapInfo3, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0.5"); + BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 1 + }; + Assert.True(criteria.Matches(beatmapInfo4, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "99"); + BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 100 + }; + Assert.True(criteria.Matches(beatmapInfo5, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0.01"); + BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 1000, + EndTimeObjectCount = 1 + }; + Assert.True(criteria.Matches(beatmapInfo6, new FilterCriteria())); + } + + [TestCase] + public void TestInvalidLnsFilters() + { + var criteria = new ManiaFilterCriteria(); + + Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "some text")); + Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "50,some text")); + Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1some text")); + } } } From 556c2469bf8b0660732f05d86695e8f9d9fc047b Mon Sep 17 00:00:00 2001 From: Binwalker Date: Mon, 25 Aug 2025 22:16:06 +0900 Subject: [PATCH 29/57] fix(ManiaFilterCriteria): converted beatmaps are not included --- .../ManiaFilterCriteria.cs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index 3b5736ad9f..60dd8e1dae 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania int keyCount = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods); bool keyCountMatch = includedKeyCounts.Contains(keyCount); - bool longNoteRatioMatch = !longNoteRatio.HasFilter || longNoteRatio.IsInRange(calculatelongNoteRatio(beatmapInfo)); + bool longNoteRatioMatch = !longNoteRatio.HasFilter || (!isConvertedBeatMap(beatmapInfo, criteria) && longNoteRatio.IsInRange(calculateLongNoteRatio(beatmapInfo))); return keyCountMatch && longNoteRatioMatch; } @@ -98,15 +98,6 @@ namespace osu.Game.Rulesets.Mania return false; } - private static float calculatelongNoteRatio(BeatmapInfo beatmapInfo) - { - int holdNotes = beatmapInfo.EndTimeObjectCount; - int totalNotes = beatmapInfo.TotalObjectCount; - int sum = Math.Max(1, totalNotes); - - return holdNotes / (float)sum * 100; - } - public bool FilterMayChangeFromMods(ValueChangedEvent> mods) { if (includedKeyCounts.Count != LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT) @@ -121,5 +112,19 @@ namespace osu.Game.Rulesets.Mania return false; } + + private static bool isConvertedBeatMap(BeatmapInfo beatmapInfo, FilterCriteria criteria) + { + return criteria.Ruleset == null || beatmapInfo.Ruleset.ShortName != criteria.Ruleset!.ShortName; + } + + private static float calculateLongNoteRatio(BeatmapInfo beatmapInfo) + { + int holdNotes = beatmapInfo.EndTimeObjectCount; + int totalNotes = beatmapInfo.TotalObjectCount; + int sum = Math.Max(1, totalNotes); + + return holdNotes / (float)sum * 100; + } } } From 65253708d8e939f237eb667d7797ee2c114df9f8 Mon Sep 17 00:00:00 2001 From: Binwalker Date: Mon, 25 Aug 2025 22:16:30 +0900 Subject: [PATCH 30/57] test(ManiaFilterCriteriaTest): fix some test case for ln filter --- .../ManiaFilterCriteriaTest.cs | 94 ++++++++++++++----- 1 file changed, 68 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs index a7686c7320..885390a052 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs @@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Mania.Tests } [TestCase] - public void TestFilterIntersection() + public void TestKeysFilterIntersection() { var criteria = new ManiaFilterCriteria(); criteria.TryParseCustomKeywordCriteria("keys", Operator.Greater, "4"); @@ -185,9 +185,13 @@ namespace osu.Game.Rulesets.Mania.Tests } [TestCase] - public void TestLnsEqualSingleValue() + public void TestLnsEqual() { var criteria = new ManiaFilterCriteria(); + var filterCriteria = new FilterCriteria + { + Ruleset = new RulesetInfo { ShortName = "mania" } + }; criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "50"); BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -195,7 +199,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 50 }; - Assert.True(criteria.Matches(beatmapInfo1, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo1, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0"); BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -203,14 +207,15 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 0, EndTimeObjectCount = 0 }; - Assert.True(criteria.Matches(beatmapInfo2, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo2, filterCriteria)); + criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0"); BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { TotalObjectCount = 100, EndTimeObjectCount = 0 }; - Assert.True(criteria.Matches(beatmapInfo3, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo3, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "1"); BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -218,7 +223,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 1 }; - Assert.True(criteria.Matches(beatmapInfo4, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo4, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "100"); BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -226,7 +231,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 100 }; - Assert.True(criteria.Matches(beatmapInfo5, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo5, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0.1"); BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -234,13 +239,17 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 1000, EndTimeObjectCount = 1 }; - Assert.True(criteria.Matches(beatmapInfo6, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo6, filterCriteria)); } [TestCase] public void TestLnsNotEqual() { var criteria = new ManiaFilterCriteria(); + var filterCriteria = new FilterCriteria + { + Ruleset = new RulesetInfo { ShortName = "mania" } + }; criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "50"); BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -248,7 +257,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 50 }; - Assert.False(criteria.Matches(beatmapInfo1, new FilterCriteria())); + Assert.False(criteria.Matches(beatmapInfo1, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "0"); BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -256,14 +265,15 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 0, EndTimeObjectCount = 0 }; - Assert.False(criteria.Matches(beatmapInfo2, new FilterCriteria())); + Assert.False(criteria.Matches(beatmapInfo2, filterCriteria)); + criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "0"); BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { TotalObjectCount = 100, EndTimeObjectCount = 0 }; - Assert.False(criteria.Matches(beatmapInfo3, new FilterCriteria())); + Assert.False(criteria.Matches(beatmapInfo3, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "1"); BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -271,7 +281,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 1 }; - Assert.False(criteria.Matches(beatmapInfo4, new FilterCriteria())); + Assert.False(criteria.Matches(beatmapInfo4, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "100"); BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -279,7 +289,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 100 }; - Assert.False(criteria.Matches(beatmapInfo5, new FilterCriteria())); + Assert.False(criteria.Matches(beatmapInfo5, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "0.1"); BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -287,13 +297,25 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 1000, EndTimeObjectCount = 1 }; - Assert.False(criteria.Matches(beatmapInfo6, new FilterCriteria())); + Assert.False(criteria.Matches(beatmapInfo6, filterCriteria)); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "50"); + BeatmapInfo beatmapInfo7 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 0 + }; + Assert.True(criteria.Matches(beatmapInfo7, filterCriteria)); } [TestCase] public void TestLnsGreaterOrEqual() { var criteria = new ManiaFilterCriteria(); + var filterCriteria = new FilterCriteria + { + Ruleset = new RulesetInfo { ShortName = "mania" } + }; criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "50"); BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -301,7 +323,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 50 }; - Assert.True(criteria.Matches(beatmapInfo1, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo1, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0"); BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -309,14 +331,15 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 0, EndTimeObjectCount = 0 }; - Assert.True(criteria.Matches(beatmapInfo2, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo2, filterCriteria)); + criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0"); BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { TotalObjectCount = 100, EndTimeObjectCount = 0 }; - Assert.True(criteria.Matches(beatmapInfo3, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo3, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1"); BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -324,7 +347,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 1 }; - Assert.True(criteria.Matches(beatmapInfo4, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo4, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "100"); BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -332,7 +355,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 100 }; - Assert.True(criteria.Matches(beatmapInfo5, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo5, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0.1"); BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -340,13 +363,17 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 1000, EndTimeObjectCount = 1 }; - Assert.True(criteria.Matches(beatmapInfo6, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo6, filterCriteria)); } [TestCase] public void TestLnsGreater() { var criteria = new ManiaFilterCriteria(); + var filterCriteria = new FilterCriteria + { + Ruleset = new RulesetInfo { ShortName = "mania" } + }; criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "49"); BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -354,7 +381,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 50 }; - Assert.True(criteria.Matches(beatmapInfo1, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo1, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0"); BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -362,14 +389,15 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 0, EndTimeObjectCount = 0 }; - Assert.False(criteria.Matches(beatmapInfo2, new FilterCriteria())); + Assert.False(criteria.Matches(beatmapInfo2, filterCriteria)); + criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0"); BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { TotalObjectCount = 100, EndTimeObjectCount = 0 }; - Assert.False(criteria.Matches(beatmapInfo3, new FilterCriteria())); + Assert.False(criteria.Matches(beatmapInfo3, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0.5"); BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -377,7 +405,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 1 }; - Assert.True(criteria.Matches(beatmapInfo4, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo4, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "99"); BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -385,7 +413,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 100 }; - Assert.True(criteria.Matches(beatmapInfo5, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo5, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0.01"); BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -393,7 +421,21 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 1000, EndTimeObjectCount = 1 }; - Assert.True(criteria.Matches(beatmapInfo6, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo6, filterCriteria)); + } + + [TestCase] + public void TestLnsNotManiaRuleset() + { + var criteria = new ManiaFilterCriteria(); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.LessOrEqual, "100"); + BeatmapInfo beatmapInfo = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 50 + }; + Assert.False(criteria.Matches(beatmapInfo, new FilterCriteria())); } [TestCase] From 6a82b7331fd0da4208d3f3c760df44717b83ee7c Mon Sep 17 00:00:00 2001 From: Binwalker Date: Tue, 26 Aug 2025 21:27:57 +0900 Subject: [PATCH 31/57] refactor(ManiaFilterCriteria): exclude converted beatmaps from long note filter --- .../ManiaFilterCriteria.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index 60dd8e1dae..3f7a018dd1 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -20,16 +20,16 @@ namespace osu.Game.Rulesets.Mania public class ManiaFilterCriteria : IRulesetFilterCriteria { private readonly HashSet includedKeyCounts = Enumerable.Range(1, LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT).ToHashSet(); - private FilterCriteria.OptionalRange longNoteRatio; + private FilterCriteria.OptionalRange longNotePercentage; public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) { int keyCount = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods); bool keyCountMatch = includedKeyCounts.Contains(keyCount); - bool longNoteRatioMatch = !longNoteRatio.HasFilter || (!isConvertedBeatMap(beatmapInfo, criteria) && longNoteRatio.IsInRange(calculateLongNoteRatio(beatmapInfo))); + bool longNotePercentageMatch = !longNotePercentage.HasFilter || (!isConvertedBeatmap(beatmapInfo) && longNotePercentage.IsInRange(calculateLongNotePercentage(beatmapInfo))); - return keyCountMatch && longNoteRatioMatch; + return keyCountMatch && longNotePercentageMatch; } public bool TryParseCustomKeywordCriteria(string key, Operator op, string strValues) @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Mania case "ln": case "lns": - return FilterQueryParser.TryUpdateCriteriaRange(ref longNoteRatio, op, strValues); + return FilterQueryParser.TryUpdateCriteriaRange(ref longNotePercentage, op, strValues); } return false; @@ -113,18 +113,17 @@ namespace osu.Game.Rulesets.Mania return false; } - private static bool isConvertedBeatMap(BeatmapInfo beatmapInfo, FilterCriteria criteria) + private static bool isConvertedBeatmap(BeatmapInfo beatmapInfo) { - return criteria.Ruleset == null || beatmapInfo.Ruleset.ShortName != criteria.Ruleset!.ShortName; + return !beatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo); } - private static float calculateLongNoteRatio(BeatmapInfo beatmapInfo) + private static float calculateLongNotePercentage(BeatmapInfo beatmapInfo) { int holdNotes = beatmapInfo.EndTimeObjectCount; - int totalNotes = beatmapInfo.TotalObjectCount; - int sum = Math.Max(1, totalNotes); + int totalNotes = Math.Max(1, beatmapInfo.TotalObjectCount); - return holdNotes / (float)sum * 100; + return holdNotes / (float)totalNotes * 100; } } } From 149f18c3f549e38ffbb7cf1c18c50f829c513b40 Mon Sep 17 00:00:00 2001 From: Binwalker Date: Tue, 26 Aug 2025 21:28:45 +0900 Subject: [PATCH 32/57] test(ManiaFilterCriteriaTest): simplify test case --- .../ManiaFilterCriteriaTest.cs | 185 +++--------------- 1 file changed, 24 insertions(+), 161 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs index 885390a052..ad3cf4e05f 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs @@ -190,30 +190,30 @@ namespace osu.Game.Rulesets.Mania.Tests var criteria = new ManiaFilterCriteria(); var filterCriteria = new FilterCriteria { - Ruleset = new RulesetInfo { ShortName = "mania" } + Ruleset = new ManiaRuleset().RulesetInfo }; - criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "50"); + criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0"); BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { - TotalObjectCount = 100, - EndTimeObjectCount = 50 + TotalObjectCount = 0, + EndTimeObjectCount = 0 }; Assert.True(criteria.Matches(beatmapInfo1, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0"); BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { - TotalObjectCount = 0, + TotalObjectCount = 100, EndTimeObjectCount = 0 }; Assert.True(criteria.Matches(beatmapInfo2, filterCriteria)); - criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0"); + criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "100"); BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { TotalObjectCount = 100, - EndTimeObjectCount = 0 + EndTimeObjectCount = 100 }; Assert.True(criteria.Matches(beatmapInfo3, filterCriteria)); @@ -225,87 +225,13 @@ namespace osu.Game.Rulesets.Mania.Tests }; Assert.True(criteria.Matches(beatmapInfo4, filterCriteria)); - criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "100"); + criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0.1"); BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { - TotalObjectCount = 100, - EndTimeObjectCount = 100 + TotalObjectCount = 1000, + EndTimeObjectCount = 1 }; Assert.True(criteria.Matches(beatmapInfo5, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0.1"); - BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 1000, - EndTimeObjectCount = 1 - }; - Assert.True(criteria.Matches(beatmapInfo6, filterCriteria)); - } - - [TestCase] - public void TestLnsNotEqual() - { - var criteria = new ManiaFilterCriteria(); - var filterCriteria = new FilterCriteria - { - Ruleset = new RulesetInfo { ShortName = "mania" } - }; - - criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "50"); - BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 50 - }; - Assert.False(criteria.Matches(beatmapInfo1, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "0"); - BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 0, - EndTimeObjectCount = 0 - }; - Assert.False(criteria.Matches(beatmapInfo2, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "0"); - BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 0 - }; - Assert.False(criteria.Matches(beatmapInfo3, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "1"); - BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 1 - }; - Assert.False(criteria.Matches(beatmapInfo4, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "100"); - BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 100 - }; - Assert.False(criteria.Matches(beatmapInfo5, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "0.1"); - BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 1000, - EndTimeObjectCount = 1 - }; - Assert.False(criteria.Matches(beatmapInfo6, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "50"); - BeatmapInfo beatmapInfo7 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 0 - }; - Assert.True(criteria.Matches(beatmapInfo7, filterCriteria)); } [TestCase] @@ -314,30 +240,30 @@ namespace osu.Game.Rulesets.Mania.Tests var criteria = new ManiaFilterCriteria(); var filterCriteria = new FilterCriteria { - Ruleset = new RulesetInfo { ShortName = "mania" } + Ruleset = new ManiaRuleset().RulesetInfo }; - criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "50"); + criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0"); BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { - TotalObjectCount = 100, - EndTimeObjectCount = 50 + TotalObjectCount = 0, + EndTimeObjectCount = 0 }; Assert.True(criteria.Matches(beatmapInfo1, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0"); BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { - TotalObjectCount = 0, + TotalObjectCount = 100, EndTimeObjectCount = 0 }; Assert.True(criteria.Matches(beatmapInfo2, filterCriteria)); - criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0"); + criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "100"); BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { TotalObjectCount = 100, - EndTimeObjectCount = 0 + EndTimeObjectCount = 100 }; Assert.True(criteria.Matches(beatmapInfo3, filterCriteria)); @@ -349,93 +275,31 @@ namespace osu.Game.Rulesets.Mania.Tests }; Assert.True(criteria.Matches(beatmapInfo4, filterCriteria)); - criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "100"); - BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 100 - }; - Assert.True(criteria.Matches(beatmapInfo5, filterCriteria)); - criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0.1"); - BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 1000, - EndTimeObjectCount = 1 - }; - Assert.True(criteria.Matches(beatmapInfo6, filterCriteria)); - } - - [TestCase] - public void TestLnsGreater() - { - var criteria = new ManiaFilterCriteria(); - var filterCriteria = new FilterCriteria - { - Ruleset = new RulesetInfo { ShortName = "mania" } - }; - - criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "49"); - BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 50 - }; - Assert.True(criteria.Matches(beatmapInfo1, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0"); - BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 0, - EndTimeObjectCount = 0 - }; - Assert.False(criteria.Matches(beatmapInfo2, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0"); - BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 0 - }; - Assert.False(criteria.Matches(beatmapInfo3, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0.5"); - BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 1 - }; - Assert.True(criteria.Matches(beatmapInfo4, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "99"); BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 100 - }; - Assert.True(criteria.Matches(beatmapInfo5, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0.01"); - BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { TotalObjectCount = 1000, EndTimeObjectCount = 1 }; - Assert.True(criteria.Matches(beatmapInfo6, filterCriteria)); + Assert.True(criteria.Matches(beatmapInfo5, filterCriteria)); } [TestCase] public void TestLnsNotManiaRuleset() { var criteria = new ManiaFilterCriteria(); + var filterCriteria = new FilterCriteria + { + Ruleset = new ManiaRuleset().RulesetInfo + }; criteria.TryParseCustomKeywordCriteria("lns", Operator.LessOrEqual, "100"); - BeatmapInfo beatmapInfo = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + BeatmapInfo beatmapInfo = new BeatmapInfo { TotalObjectCount = 100, EndTimeObjectCount = 50 }; - Assert.False(criteria.Matches(beatmapInfo, new FilterCriteria())); + Assert.False(criteria.Matches(beatmapInfo, filterCriteria)); } [TestCase] @@ -444,7 +308,6 @@ namespace osu.Game.Rulesets.Mania.Tests var criteria = new ManiaFilterCriteria(); Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "some text")); - Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "50,some text")); Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1some text")); } } From 244bad07c7c66e8921289c4bbfb07256b1697fec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Aug 2025 21:22:27 +0900 Subject: [PATCH 33/57] Update framework --- osu.Android.props | 2 +- osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs | 2 ++ osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 010413a869..40a9b454ce 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index e8f8cae90d..7545031cf3 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 043235fed25bf00eadc42caa0d2611dfbc86cdd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Aug 2025 18:03:37 +0900 Subject: [PATCH 34/57] Add test coverage ensuring filtering does not occur on unnecessary updates --- .../TestSceneBeatmapCarouselUpdateHandling.cs | 81 +++++++++++++++++-- osu.Game/Graphics/Carousel/Carousel.cs | 2 +- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs index eb610a40f1..3638c8eeec 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs @@ -22,12 +22,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { private BeatmapSetInfo baseTestBeatmap = null!; + private const int initial_filter_count = 3; + [SetUpSteps] public void SetUpSteps() { RemoveAllBeatmaps(); CreateCarousel(); + WaitForFiltering(); AddBeatmaps(1, 3); + WaitForFiltering(); AddStep("generate and add test beatmap", () => { baseTestBeatmap = TestResources.CreateTestBeatmapSetInfo(3); @@ -42,8 +46,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 b.Metadata = metadata; BeatmapSets.Add(baseTestBeatmap); }); - WaitForFiltering(); + + AddAssert("filter count correct", () => Carousel.FilterCount, () => Is.EqualTo(initial_filter_count)); } [Test] @@ -81,12 +86,18 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddUntilStep("is scrolled to end", () => Carousel.ChildrenOfType().Single().IsScrolledToEnd()); - updateBeatmap(b => b.Metadata = new BeatmapMetadata + updateBeatmap(b => { - Artist = "updated test", - Title = $"beatmap {RNG.Next().ToString()}" + // hash will be updated when important metadata changes, such as title, difficulty, author etc. + b.Hash = "new hash"; + b.Metadata = new BeatmapMetadata + { + Artist = "updated test", + Title = $"beatmap {RNG.Next().ToString()}" + }; }); + assertDidFilter(); WaitForFiltering(); AddAssert("scroll is still at end", () => Carousel.ChildrenOfType().Single().IsScrolledToEnd()); @@ -113,8 +124,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("find panel", () => panel = Carousel.ChildrenOfType().Single(p => p.ChildrenOfType().Any(t => t.Text.ToString() == "beatmap"))); - updateBeatmap(b => b.Metadata = metadata); + updateBeatmap(b => + { + b.Metadata = metadata; + // hash will be updated when important metadata changes, such as title, difficulty, author etc. + b.Hash = "new hash"; + }); + assertDidFilter(); WaitForFiltering(); AddAssert("drawables unchanged", () => Carousel.ChildrenOfType(), () => Is.EqualTo(originalDrawables)); @@ -123,7 +140,41 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } [Test] - public void TestSelectionHeld() + public void TestOnlineStatusUpdated() + { + List originalDrawables = new List(); + + AddStep("store drawable references", () => + { + originalDrawables.Clear(); + originalDrawables.AddRange(Carousel.ChildrenOfType().ToList()); + }); + + updateBeatmap(b => b.Status = BeatmapOnlineStatus.Graveyard); + + assertDidFilter(); + WaitForFiltering(); + + AddAssert("drawables unchanged", () => Carousel.ChildrenOfType(), () => Is.EqualTo(originalDrawables)); + } + + [Test] + public void TestNoUpdateTriggeredOnUserTagsChange() + { + var metadata = new BeatmapMetadata + { + Artist = "updated test", + Title = "new beatmap title", + UserTags = { "hi" } + }; + + updateBeatmap(b => b.Metadata = metadata); + assertDidNotFilter(); + } + + [TestCase(false)] + [TestCase(true)] + public void TestSelectionHeld(bool hashChanged) { SelectNextSet(); @@ -131,7 +182,17 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); - updateBeatmap(); + updateBeatmap(b => + { + if (hashChanged) + b.Hash = "new hash"; + }); + + if (hashChanged) + assertDidFilter(); + else + assertDidNotFilter(); + WaitForFiltering(); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); @@ -148,6 +209,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); updateBeatmap(b => b.DifficultyName = "new name"); + assertDidFilter(); WaitForFiltering(); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); @@ -164,6 +226,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); updateBeatmap(b => b.OnlineID = b.OnlineID + 1); + assertDidFilter(); WaitForFiltering(); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); @@ -339,6 +402,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("Order didn't change", () => Carousel.PostFilterBeatmaps.Select(b => b.ID), () => Is.EqualTo(originalOrder)); } + private void assertDidFilter() => AddAssert("did filter", () => Carousel.FilterCount, () => Is.EqualTo(initial_filter_count + 1)); + + private void assertDidNotFilter() => AddAssert("did not filter", () => Carousel.FilterCount, () => Is.EqualTo(initial_filter_count)); + private void updateBeatmap(Action? updateBeatmap = null, Action? updateSet = null) { AddStep("update beatmap with different reference", () => diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 7b5aea08b6..c9a3c7f723 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -79,7 +79,7 @@ namespace osu.Game.Graphics.Carousel /// /// The number of times filter operations have been triggered. /// - internal int FilterCount { get; private set; } + public int FilterCount { get; private set; } /// /// The number of displayable items currently being tracked (before filtering). From 0e57ee9ba68f0fb7b58b8f11efd4611ff4e9d936 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Aug 2025 18:04:19 +0900 Subject: [PATCH 35/57] Avoid triggering changes when add operations are empty Only seems to happen in tests. I think. --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index da841aa361..10ce578562 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -143,6 +143,9 @@ namespace osu.Game.Screens.SelectV2 switch (changed.Action) { case NotifyCollectionChangedAction.Add: + if (!newItems!.Any()) + return; + Items.AddRange(newItems!.SelectMany(s => s.Beatmaps)); break; From be6fb9aa776f83e1b9560329f6a5033a5b1476b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Aug 2025 18:04:58 +0900 Subject: [PATCH 36/57] Fix beatmap carousel re-filtering when it doesn't need to Local rules ensure we only handle callbacks when we need to. --- osu.Game/Graphics/Carousel/Carousel.cs | 13 +++++- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 46 ++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index c9a3c7f723..b81df0a7eb 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using System.Threading; @@ -210,6 +211,12 @@ namespace osu.Game.Graphics.Carousel return filterTask; } + /// + /// Called when changes in any way. + /// + /// Whether a re-filter is required. + protected virtual bool HandleItemsChanged(NotifyCollectionChangedEventArgs args) => true; + /// /// Fired after a filter operation completed. /// @@ -301,7 +308,11 @@ namespace osu.Game.Graphics.Carousel RelativeSizeAxes = Axes.Both, }; - Items.BindCollectionChanged((_, _) => filterAfterItemsChanged.Invalidate()); + Items.BindCollectionChanged((_, args) => + { + if (HandleItemsChanged(args)) + filterAfterItemsChanged.Invalidate(); + }); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 10ce578562..ad691d34c0 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -356,6 +356,52 @@ namespace osu.Game.Screens.SelectV2 } } + protected override bool HandleItemsChanged(NotifyCollectionChangedEventArgs args) + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + case NotifyCollectionChangedAction.Remove: + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Reset: + return true; + + case NotifyCollectionChangedAction.Replace: + var oldBeatmaps = args.OldItems!.OfType().ToList(); + var newBeatmaps = args.NewItems!.OfType().ToList(); + + for (int i = 0; i < oldBeatmaps.Count; i++) + { + var oldBeatmap = oldBeatmaps[i]; + var newBeatmap = newBeatmaps[i]; + + // Ignore changes which don't concern us. + // + // Here are some examples of things that can go wrong: + // - Background difficulty calculation runs and causes a realm update. + // We use `BeatmapDifficultyCache` and don't want to know about these. + // - Background user tag population runs and causes a realm update. + // We don't display user tags so want to ignore this. + if ( + // covers metadata changes + oldBeatmap.Hash == newBeatmap.Hash && + // displayed + oldBeatmap.Status == newBeatmap.Status && + // displayed + oldBeatmap.DifficultyName == newBeatmap.DifficultyName && + // sanity + oldBeatmap.OnlineID == newBeatmap.OnlineID + ) + return false; + } + + return true; + + default: + throw new ArgumentOutOfRangeException(); + } + } + protected override void HandleFilterCompleted() { base.HandleFilterCompleted(); From fda40d7fd5a3f5657e6c495202ee23719ebb1890 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Aug 2025 18:31:27 +0900 Subject: [PATCH 37/57] Fix beatmap panels locally handling mod changes unnecessarily The `BeatmapDifficultyCache` handles mod changes, so handling locally is unnecessary. By handling locally, it creates a visual issue when adjusting mods often. Test using Ctrl +/- at song select and observing that without this change, the star rating will flicker back to the default due to the local re-fetch. --- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index d91864ed95..2475e32a39 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -205,11 +205,7 @@ namespace osu.Game.Screens.SelectV2 updateKeyCount(); }); - mods.BindValueChanged(_ => - { - computeStarRating(); - updateKeyCount(); - }, true); + mods.BindValueChanged(_ => updateKeyCount(), true); } protected override void PrepareForUse() From 197c318180b8030ec191a398d0ef1dcb008e8017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Aug 2025 09:35:49 +0200 Subject: [PATCH 38/57] Fix `HealthProcessor` potentially incorrectly reverting failed state This stems from me looking into `TestSceneFailAnimation` failures (https://github.com/ppy/osu/runs/48663953318). As it turns out, I should not have been mad by CI, and rather should have been mad at myself for failing to read. `FailedAtJudgement` in fact does not mean "this judgement, and only this judgement, triggered failure". If any further judgements occur post-fail, they will also have `FailedAtJudgement` set to true. It is essentially a *dump* of the state of `HealthProcessor.Failed` prior to applying the judgement. https://github.com/ppy/osu/blob/ec21685c2531af3b243f7f0833ffbb340bf3c044/osu.Game/Rulesets/Scoring/HealthProcessor.cs#L49-L57 Because of this, reverting several judgements which occur post-fail could lead to failed state reverting earlier than intended, and thus potentially trigger a second fail, thus tripping the `Player` assertion. --- osu.Game/Rulesets/Scoring/HealthProcessor.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index 501b0a84bc..d61e41f867 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -59,11 +59,7 @@ namespace osu.Game.Rulesets.Scoring protected override void RevertResultInternal(JudgementResult result) { - // TODO: this is rudimentary as to make rewinding failed replays work, - // but it also acts up (sometimes rewinding a replay several times around the fail boundary moves the point of fail forward). - // needs further investigation. - if (result.FailedAtJudgement) - HasFailed = false; + HasFailed = result.FailedAtJudgement; if (HasFailed) return; From f9c1b24df4aab65a50253ddd2593097e6407a94c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Aug 2025 19:59:44 +0900 Subject: [PATCH 39/57] Apply in more places --- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 7 +------ osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs | 13 ++----------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 2475e32a39..106b911606 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -199,12 +199,7 @@ namespace osu.Game.Screens.SelectV2 { base.LoadComplete(); - ruleset.BindValueChanged(_ => - { - computeStarRating(); - updateKeyCount(); - }); - + ruleset.BindValueChanged(_ => updateKeyCount()); mods.BindValueChanged(_ => updateKeyCount(), true); } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index b443b32dbc..87a35facbd 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -209,17 +209,8 @@ namespace osu.Game.Screens.SelectV2 { base.LoadComplete(); - ruleset.BindValueChanged(_ => - { - computeStarRating(); - updateKeyCount(); - }); - - mods.BindValueChanged(_ => - { - computeStarRating(); - updateKeyCount(); - }, true); + ruleset.BindValueChanged(_ => updateKeyCount()); + mods.BindValueChanged(_ => updateKeyCount(), true); Selected.BindValueChanged(s => Expanded.Value = s.NewValue, true); } From 40303832761831e74ac31175205151257bf9030c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Aug 2025 14:45:20 +0200 Subject: [PATCH 40/57] Allow grouping modes that apply max aggregate to split beatmap sets apart --- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 32 +++---------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index f17281db2f..f0ec3ae3ab 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -43,7 +43,8 @@ namespace osu.Game.Screens.SelectV2 private readonly Func> getCollections; private readonly Func> getLocalUserTopRanks; - public BeatmapCarouselFilterGrouping(Func getCriteria, Func> getCollections, Func> getLocalUserTopRanks) + public BeatmapCarouselFilterGrouping(Func getCriteria, Func> getCollections, + Func> getLocalUserTopRanks) { this.getCriteria = getCriteria; this.getCollections = getCollections; @@ -189,9 +190,6 @@ namespace osu.Game.Screens.SelectV2 { var date = b.LastPlayed; - if (BeatmapSetsGroupedTogether) - date = aggregateMax(b, static b => b.LastPlayed ?? DateTimeOffset.MinValue); - if (date == null || date == DateTimeOffset.MinValue) return new GroupDefinition(int.MaxValue, "Never"); @@ -202,29 +200,13 @@ namespace osu.Game.Screens.SelectV2 return getGroupsBy(b => defineGroupByStatus(b.BeatmapSet!.Status), items); case GroupMode.BPM: - return getGroupsBy(b => - { - double bpm = FormatUtils.RoundBPM(b.BPM); - - if (BeatmapSetsGroupedTogether) - bpm = aggregateMax(b, bb => FormatUtils.RoundBPM(bb.BPM)); - - return defineGroupByBPM(bpm); - }, items); + return getGroupsBy(b => defineGroupByBPM(FormatUtils.RoundBPM(b.BPM)), items); case GroupMode.Difficulty: return getGroupsBy(b => defineGroupByStars(b.StarRating), items); case GroupMode.Length: - return getGroupsBy(b => - { - double length = b.Length; - - if (BeatmapSetsGroupedTogether) - length = aggregateMax(b, bb => bb.Length); - - return defineGroupByLength(length); - }, items); + return getGroupsBy(b => defineGroupByLength(b.Length), items); case GroupMode.Source: return getGroupsBy(b => defineGroupBySource(b.BeatmapSet!.Metadata.Source), items); @@ -433,12 +415,6 @@ namespace osu.Game.Screens.SelectV2 return new GroupDefinition(int.MaxValue, "Unplayed"); } - private static T? aggregateMax(BeatmapInfo b, Func func) - { - var beatmaps = b.BeatmapSet!.Beatmaps.Where(bb => !bb.Hidden); - return beatmaps.Max(func); - } - private record GroupMapping(GroupDefinition? Group, List ItemsInGroup); } } From 8a6c8577192e61aa475be2e461a0529d2501fbfd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Aug 2025 02:33:18 +0900 Subject: [PATCH 41/57] Fix hidden beatmap state not being reflected immediately --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index ad691d34c0..5b0ad1ae29 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -382,16 +382,19 @@ namespace osu.Game.Screens.SelectV2 // We use `BeatmapDifficultyCache` and don't want to know about these. // - Background user tag population runs and causes a realm update. // We don't display user tags so want to ignore this. - if ( + bool equalForDisplayPurposes = // covers metadata changes oldBeatmap.Hash == newBeatmap.Hash && - // displayed + // sanity check + oldBeatmap.OnlineID == newBeatmap.OnlineID && + // displayed on panel oldBeatmap.Status == newBeatmap.Status && - // displayed + // displayed on panel oldBeatmap.DifficultyName == newBeatmap.DifficultyName && - // sanity - oldBeatmap.OnlineID == newBeatmap.OnlineID - ) + // hidden changed, needs re-filter + oldBeatmap.Hidden == newBeatmap.Hidden; + + if (equalForDisplayPurposes) return false; } From e831d1b6fa3110740bb4faa3113ac813b9bfb54f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 28 Aug 2025 13:06:14 +0900 Subject: [PATCH 42/57] Preserve pre-post notification completion target --- osu.Game/Overlays/NotificationOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 18a487a312..f56e5e6ac3 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -180,7 +180,7 @@ namespace osu.Game.Overlays notification.Closed += () => notificationClosed(notification); if (notification is IHasCompletionTarget hasCompletionTarget) - hasCompletionTarget.CompletionTarget = Post; + hasCompletionTarget.CompletionTarget ??= Post; playDebouncedSample(notification.PopInSampleName); From 311c75aa533979521981aa5ed70494d3c57a4f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Aug 2025 10:26:31 +0200 Subject: [PATCH 43/57] Adjust test after allowing grouping modes to split beatmap sets apart --- .../BeatmapCarouselFilterGroupingTest.cs | 107 +++++++++--------- 1 file changed, 54 insertions(+), 53 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs index c8f1c1e017..592994f2f0 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -74,11 +74,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(applyBeatmap('_'), beatmapSets, out var underscoreBeatmap); var results = await runGrouping(mode, beatmapSets); - assertGroup(results, 0, "0-9", new[] { fiveBeatmap, fourBeatmap }, ref total); - assertGroup(results, 1, "A", new[] { aBeatmap }, ref total); - assertGroup(results, 2, "F", new[] { fBeatmap }, ref total); - assertGroup(results, 3, "Z", new[] { zBeatmap }, ref total); - assertGroup(results, 4, "Other", new[] { dashBeatmap, underscoreBeatmap }, ref total); + assertGroup(results, 0, "0-9", fiveBeatmap.Beatmaps.Concat(fourBeatmap.Beatmaps), ref total); + assertGroup(results, 1, "A", aBeatmap.Beatmaps, ref total); + assertGroup(results, 2, "F", fBeatmap.Beatmaps, ref total); + assertGroup(results, 3, "Z", zBeatmap.Beatmaps, ref total); + assertGroup(results, 4, "Other", dashBeatmap.Beatmaps.Concat(underscoreBeatmap.Beatmaps), ref total); assertTotal(results, total); } @@ -115,12 +115,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(s => s.DateAdded = DateTimeOffset.Now.AddMonths(-2).AddDays(-3), beatmapSets, out var twoMonthsAgoBeatmap); var results = await runGrouping(GroupMode.DateAdded, beatmapSets); - assertGroup(results, 0, "Today", new[] { todayBeatmap }, ref total); - assertGroup(results, 1, "Yesterday", new[] { yesterdayBeatmap }, ref total); - assertGroup(results, 2, "Last week", new[] { lastWeekBeatmap }, ref total); - assertGroup(results, 3, "Last month", new[] { lastMonthBeatmap }, ref total); - assertGroup(results, 4, "1 month ago", new[] { oneMonthAgoBeatmap }, ref total); - assertGroup(results, 5, "2 months ago", new[] { twoMonthsAgoBeatmap }, ref total); + assertGroup(results, 0, "Today", todayBeatmap.Beatmaps, ref total); + assertGroup(results, 1, "Yesterday", yesterdayBeatmap.Beatmaps, ref total); + assertGroup(results, 2, "Last week", lastWeekBeatmap.Beatmaps, ref total); + assertGroup(results, 3, "Last month", lastMonthBeatmap.Beatmaps, ref total); + assertGroup(results, 4, "1 month ago", oneMonthAgoBeatmap.Beatmaps, ref total); + assertGroup(results, 5, "2 months ago", twoMonthsAgoBeatmap.Beatmaps, ref total); assertTotal(results, total); } @@ -139,13 +139,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(applyLastPlayed(null), beatmapSets, out var neverBeatmap); var results = await runGrouping(GroupMode.LastPlayed, beatmapSets); - assertGroup(results, 0, "Today", new[] { todayBeatmap }, ref total); - assertGroup(results, 1, "Yesterday", new[] { yesterdayBeatmap }, ref total); - assertGroup(results, 2, "Last week", new[] { lastWeekBeatmap }, ref total); - assertGroup(results, 3, "Last month", new[] { lastMonthBeatmap }, ref total); - assertGroup(results, 4, "1 month ago", new[] { oneMonthAgoBeatmap }, ref total); - assertGroup(results, 5, "2 months ago", new[] { twoMonthsBeatmap }, ref total); - assertGroup(results, 6, "Never", new[] { neverBeatmap }, ref total); + assertGroup(results, 0, "Today", todayBeatmap.Beatmaps, ref total); + assertGroup(results, 1, "Yesterday", yesterdayBeatmap.Beatmaps, ref total); + assertGroup(results, 2, "Last week", lastWeekBeatmap.Beatmaps, ref total); + assertGroup(results, 3, "Last month", lastMonthBeatmap.Beatmaps, ref total); + assertGroup(results, 4, "1 month ago", oneMonthAgoBeatmap.Beatmaps, ref total); + assertGroup(results, 5, "2 months ago", twoMonthsBeatmap.Beatmaps, ref total); + assertGroup(results, 6, "Never", neverBeatmap.Beatmaps, ref total); assertTotal(results, total); } @@ -162,7 +162,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 var results = await runGrouping(GroupMode.LastPlayed, beatmapSets); int total = 0; - assertGroup(results, 0, "Today", new[] { set }, ref total); + assertGroup(results, 0, "Today", [set.Beatmaps[2]], ref total); + assertGroup(results, 1, "Never", [set.Beatmaps[0], set.Beatmaps[1]], ref total); assertTotal(results, total); } @@ -176,8 +177,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 var results = await runGrouping(GroupMode.LastPlayed, beatmapSets); int total = 0; - assertGroup(results, 0, "Over 5 months ago", new[] { overFiveMonthsBeatmap }, ref total); - assertGroup(results, 1, "Never", new[] { neverBeatmap }, ref total); + assertGroup(results, 0, "Over 5 months ago", overFiveMonthsBeatmap.Beatmaps, ref total); + assertGroup(results, 1, "Never", neverBeatmap.Beatmaps, ref total); assertTotal(results, total); } @@ -207,14 +208,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(s => s.Status = BeatmapOnlineStatus.LocallyModified, beatmapSets, out var localBeatmap); var results = await runGrouping(GroupMode.RankedStatus, beatmapSets); - assertGroup(results, 0, "Ranked", new[] { rankedBeatmap, approvedBeatmap }, ref total); - assertGroup(results, 1, "Qualified", new[] { qualifiedBeatmap }, ref total); - assertGroup(results, 2, "WIP", new[] { wipBeatmap }, ref total); - assertGroup(results, 3, "Pending", new[] { pendingBeatmap }, ref total); - assertGroup(results, 4, "Graveyard", new[] { graveyardBeatmap }, ref total); - assertGroup(results, 5, "Local", new[] { localBeatmap }, ref total); - assertGroup(results, 6, "Unknown", new[] { noneBeatmap }, ref total); - assertGroup(results, 7, "Loved", new[] { lovedBeatmap }, ref total); + assertGroup(results, 0, "Ranked", rankedBeatmap.Beatmaps.Concat(approvedBeatmap.Beatmaps), ref total); + assertGroup(results, 1, "Qualified", qualifiedBeatmap.Beatmaps, ref total); + assertGroup(results, 2, "WIP", wipBeatmap.Beatmaps, ref total); + assertGroup(results, 3, "Pending", pendingBeatmap.Beatmaps, ref total); + assertGroup(results, 4, "Graveyard", graveyardBeatmap.Beatmaps, ref total); + assertGroup(results, 5, "Local", localBeatmap.Beatmaps, ref total); + assertGroup(results, 6, "Unknown", noneBeatmap.Beatmaps, ref total); + assertGroup(results, 7, "Loved", lovedBeatmap.Beatmaps, ref total); assertTotal(results, total); } @@ -240,12 +241,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(applyBPM(330), beatmapSets, out var beatmap330); var results = await runGrouping(GroupMode.BPM, beatmapSets); - assertGroup(results, 0, "Under 60 BPM", new[] { beatmap30 }, ref total); - assertGroup(results, 1, "60 - 70 BPM", new[] { beatmap59, beatmap60 }, ref total); - assertGroup(results, 2, "90 - 100 BPM", new[] { beatmap90, beatmap95 }, ref total); - assertGroup(results, 3, "270 - 280 BPM", new[] { beatmap269, beatmap270 }, ref total); - assertGroup(results, 4, "290 - 300 BPM", new[] { beatmap299 }, ref total); - assertGroup(results, 5, "Over 300 BPM", new[] { beatmap300, beatmap330 }, ref total); + assertGroup(results, 0, "Under 60 BPM", beatmap30.Beatmaps, ref total); + assertGroup(results, 1, "60 - 70 BPM", (beatmap59.Beatmaps.Concat(beatmap60.Beatmaps)), ref total); + assertGroup(results, 2, "90 - 100 BPM", (beatmap90.Beatmaps.Concat(beatmap95.Beatmaps)), ref total); + assertGroup(results, 3, "270 - 280 BPM", (beatmap269.Beatmaps.Concat(beatmap270.Beatmaps)), ref total); + assertGroup(results, 4, "290 - 300 BPM", beatmap299.Beatmaps, ref total); + assertGroup(results, 5, "Over 300 BPM", (beatmap300.Beatmaps.Concat(beatmap330.Beatmaps)), ref total); assertTotal(results, total); } @@ -272,10 +273,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(applyStars(7), beatmapSets, out var beatmap7); var results = await runGrouping(GroupMode.Difficulty, beatmapSets); - assertGroup(results, 0, "Below 1 Star", new[] { beatmapBelow1 }, ref total); - assertGroup(results, 1, "1 Star", new[] { beatmapAbove1, beatmapAlmost2 }, ref total); - assertGroup(results, 2, "2 Stars", new[] { beatmap2, beatmapAbove2 }, ref total); - assertGroup(results, 3, "7 Stars", new[] { beatmap7 }, ref total); + assertGroup(results, 0, "Below 1 Star", beatmapBelow1.Beatmaps, ref total); + assertGroup(results, 1, "1 Star", (beatmapAbove1.Beatmaps.Concat(beatmapAlmost2.Beatmaps)), ref total); + assertGroup(results, 2, "2 Stars", (beatmap2.Beatmaps.Concat(beatmapAbove2.Beatmaps)), ref total); + assertGroup(results, 3, "7 Stars", beatmap7.Beatmaps, ref total); assertTotal(results, total); } @@ -304,11 +305,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(applyLength(630_000), beatmapSets, out var beatmap10Min30Sec); var results = await runGrouping(GroupMode.Length, beatmapSets); - assertGroup(results, 0, "1 minute or less", new[] { beatmap30Sec, beatmap1Min }, ref total); - assertGroup(results, 1, "2 minutes or less", new[] { beatmap1Min30Sec, beatmap2Min }, ref total); - assertGroup(results, 2, "5 minutes or less", new[] { beatmap5Min }, ref total); - assertGroup(results, 3, "10 minutes or less", new[] { beatmap6Min, beatmap10Min }, ref total); - assertGroup(results, 4, "Over 10 minutes", new[] { beatmap10Min30Sec }, ref total); + assertGroup(results, 0, "1 minute or less", (beatmap30Sec.Beatmaps.Concat(beatmap1Min.Beatmaps)), ref total); + assertGroup(results, 1, "2 minutes or less", (beatmap1Min30Sec.Beatmaps.Concat(beatmap2Min.Beatmaps)), ref total); + assertGroup(results, 2, "5 minutes or less", beatmap5Min.Beatmaps, ref total); + assertGroup(results, 3, "10 minutes or less", (beatmap6Min.Beatmaps.Concat(beatmap10Min.Beatmaps)), ref total); + assertGroup(results, 4, "Over 10 minutes", beatmap10Min30Sec.Beatmaps, ref total); assertTotal(results, total); } @@ -334,10 +335,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(s => s.DateRanked = null, beatmapSets, out var beatmapUnranked); var results = await runGrouping(GroupMode.DateRanked, beatmapSets); - assertGroup(results, 0, "2025", new[] { beatmap2025 }, ref total); - assertGroup(results, 1, "2010", new[] { beatmap2010 }, ref total); - assertGroup(results, 2, "2007", new[] { beatmapOct2007, beatmapDec2007 }, ref total); - assertGroup(results, 3, "Unranked", new[] { beatmapUnranked }, ref total); + assertGroup(results, 0, "2025", beatmap2025.Beatmaps, ref total); + assertGroup(results, 1, "2010", beatmap2010.Beatmaps, ref total); + assertGroup(results, 2, "2007", (beatmapOct2007.Beatmaps.Concat(beatmapDec2007.Beatmaps)), ref total); + assertGroup(results, 3, "Unranked", beatmapUnranked.Beatmaps, ref total); assertTotal(results, total); } @@ -357,9 +358,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = string.Empty, beatmapSets, out var beatmapUnsourced); var results = await runGrouping(GroupMode.Source, beatmapSets); - assertGroup(results, 0, "Cool Game", new[] { beatmapCoolGame, beatmapCoolGameB }, ref total); - assertGroup(results, 1, "Nice Movie", new[] { beatmapNiceMovie }, ref total); - assertGroup(results, 2, "Unsourced", new[] { beatmapUnsourced }, ref total); + assertGroup(results, 0, "Cool Game", (beatmapCoolGame.Beatmaps.Concat(beatmapCoolGameB.Beatmaps)), ref total); + assertGroup(results, 1, "Nice Movie", beatmapNiceMovie.Beatmaps, ref total); + assertGroup(results, 2, "Unsourced", beatmapUnsourced.Beatmaps, ref total); assertTotal(results, total); } @@ -375,7 +376,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 return await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None); } - private static void assertGroup(List items, int index, string expectedTitle, IEnumerable expectedBeatmapSets, ref int totalItems) + private static void assertGroup(List items, int index, string expectedTitle, IEnumerable expectedBeatmaps, ref int totalItems) { var groupItem = items.Where(i => i.Model is GroupDefinition).ElementAtOrDefault(index); @@ -390,7 +391,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 var groupModel = (GroupDefinition)groupItem.Model; Assert.That(groupModel.Title, Is.EqualTo(expectedTitle)); - Assert.That(itemsInGroup.Select(i => i.Model).OfType(), Is.EquivalentTo(expectedBeatmapSets.SelectMany(bs => bs.Beatmaps))); + Assert.That(itemsInGroup.Select(i => i.Model).OfType(), Is.EquivalentTo(expectedBeatmaps)); totalItems += itemsInGroup.Count() + 1; } From 47164c61b4889a8e1af7c871ffb7c3b751ed425d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Aug 2025 14:45:32 +0200 Subject: [PATCH 44/57] Add failing test coverage of splitting beatmap sets apart --- .../TestSceneBeatmapCarouselSetsSplitApart.cs | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetsSplitApart.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetsSplitApart.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetsSplitApart.cs new file mode 100644 index 0000000000..fa635f9bde --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetsSplitApart.cs @@ -0,0 +1,120 @@ +// 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.Testing; +using osu.Game.Beatmaps; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselSetsSplitApart : BeatmapCarouselTestScene + { + [SetUpSteps] + public void SetUpSteps() + { + RemoveAllBeatmaps(); + CreateCarousel(); + + SortAndGroupBy(SortMode.Title, GroupMode.Length); + } + + [Test] + public void TestSetTraversal() + { + AddBeatmaps(3, splitApart: true); + AddBeatmaps(3, splitApart: false); + WaitForDrawablePanels(); + + SelectNextSet(); + WaitForSetSelection(set: 0, diff: 0); + + SelectNextSet(); + WaitForSetSelection(set: 1, diff: 0); + + SelectPrevSet(); + WaitForSetSelection(set: 0, diff: 0); + + SelectPrevSet(); + WaitForSetSelection(set: 5, diff: 0); + + SelectPrevSet(); + SelectPrevSet(); + SelectPrevSet(); + WaitForSetSelection(set: 2, diff: 4); + AddAssert("only two beatmap panels visible", () => GetVisiblePanels().Count(), () => Is.EqualTo(2)); + } + + [Test] + public void TestBeatmapTraversal() + { + AddBeatmaps(3, splitApart: true); + AddBeatmaps(3, splitApart: false); + WaitForDrawablePanels(); + + SelectNextSet(); + WaitForSetSelection(set: 0, diff: 0); + + SelectNextPanel(); + WaitForSetSelection(set: 0, diff: 1); + + SelectNextPanel(); // header of set 1 in group 0 + Select(); + WaitForSetSelection(set: 1, diff: 0); + + SelectPrevPanel(); // header of set 1 in group 0 + SelectPrevPanel(); // header of set 0 in group 0 + Select(); + WaitForSetSelection(set: 0, diff: 0); + + SelectPrevPanel(); // header of set 0 in group 0 + SelectPrevPanel(); // header of group 0 + SelectPrevPanel(); // header of group 2 + Select(); + SelectNextPanel(); // header of set 0 in group 2 + Select(); + WaitForSetSelection(set: 0, diff: 4); + } + + [Test] + public void TestRandomStaysInGroup() + { + AddBeatmaps(2, splitApart: false); + AddBeatmaps(1, splitApart: true); + WaitForDrawablePanels(); + + SelectPrevSet(); + SelectPrevSet(); + WaitForSetSelection(set: 1); + WaitForExpandedGroup(2); + + AddStep("select next random", () => Carousel.NextRandom()); + WaitForExpandedGroup(2); + AddStep("select next random", () => Carousel.NextRandom()); + WaitForExpandedGroup(2); + } + + protected void AddBeatmaps(int count, bool splitApart) => AddStep($"add {count} beatmaps ({(splitApart ? "" : "not ")}split apart)", () => + { + var beatmapSets = new List(); + + for (int i = 0; i < count; i++) + { + var beatmapSet = CreateTestBeatmapSetInfo(6, false); + + for (int j = 0; j < beatmapSet.Beatmaps.Count; j++) + { + beatmapSet.Beatmaps[j].Length = splitApart ? 30_000 * (j + 1) : 180_000; + } + + beatmapSets.Add(beatmapSet); + } + + BeatmapSets.AddRange(beatmapSets); + }); + } +} From 7e109add9618bf3a59d47e999fa864fc23ed11a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Aug 2025 19:10:20 +0900 Subject: [PATCH 45/57] Ensure filtering also runs after local gameplay `LastPlayed` changes --- .../SongSelectV2/TestSceneSongSelect.cs | 24 +++++++++++++++++++ osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 4 +++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 69bdb97617..895f148965 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -23,7 +23,9 @@ using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; using osuTK.Input; +using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; using FooterButtonMods = osu.Game.Screens.SelectV2.FooterButtonMods; using FooterButtonOptions = osu.Game.Screens.SelectV2.FooterButtonOptions; using FooterButtonRandom = osu.Game.Screens.SelectV2.FooterButtonRandom; @@ -302,6 +304,28 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); } + /// + /// Last played and rank achieved may have changed, so we want to make sure filtering runs on resume to song select. + /// + [Test] + public void TestFilteringRunsAfterReturningFromGameplay() + { + AddStep("import actual beatmap", () => Beatmaps.Import(TestResources.GetQuickTestBeatmapForImport())); + LoadSongSelect(); + + AddUntilStep("wait for filtered", () => SongSelect.ChildrenOfType().Single().FilterCount, () => Is.EqualTo(1)); + + AddStep("enter gameplay", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); + AddUntilStep("wait for fail", () => ((Player)Stack.CurrentScreen).GameplayState.HasFailed); + + AddStep("exit gameplay", () => InputManager.Key(Key.Escape)); + AddStep("exit gameplay", () => InputManager.Key(Key.Escape)); + + AddUntilStep("wait for filtered", () => SongSelect.ChildrenOfType().Single().FilterCount, () => Is.EqualTo(2)); + } + [Test] public void TestAutoplayShortcut() { diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 5b0ad1ae29..a4e957a1bf 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -392,7 +392,9 @@ namespace osu.Game.Screens.SelectV2 // displayed on panel oldBeatmap.DifficultyName == newBeatmap.DifficultyName && // hidden changed, needs re-filter - oldBeatmap.Hidden == newBeatmap.Hidden; + oldBeatmap.Hidden == newBeatmap.Hidden && + // might be used for grouping, returning from gameplay + oldBeatmap.LastPlayed == newBeatmap.LastPlayed; if (equalForDisplayPurposes) return false; From 8dd131f17ee57e8cbe005b8a8abb8e0ff3a0c4aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Aug 2025 09:40:22 +0200 Subject: [PATCH 46/57] Support beatmap sets being split apart by the active group mode in beatmap carousel --- .../SongSelectV2/BeatmapCarouselTestScene.cs | 2 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 70 +++++++++++-------- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 15 ++-- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 19 +++-- 4 files changed, 60 insertions(+), 46 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index bc507fbffa..64084d76f1 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -440,7 +440,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 public BeatmapInfo? SelectedBeatmapInfo => CurrentSelection as BeatmapInfo; public BeatmapSetInfo? SelectedBeatmapSet => SelectedBeatmapInfo?.BeatmapSet; - public new BeatmapSetInfo? ExpandedBeatmapSet => base.ExpandedBeatmapSet; + public new BeatmapSetUnderGrouping? ExpandedBeatmapSet => base.ExpandedBeatmapSet; public new GroupDefinition? ExpandedGroup => base.ExpandedGroup; public TestBeatmapCarousel() diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index da841aa361..95fb26c6dd 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -69,11 +70,11 @@ namespace osu.Game.Screens.SelectV2 if (grouping.BeatmapSetsGroupedTogether) { // Give some space around the expanded beatmap set, at the top.. - if (bottom.Model is BeatmapSetInfo && bottom.IsExpanded) + if (bottom.Model is BeatmapSetUnderGrouping && bottom.IsExpanded) return SPACING * 2; // ..and the bottom. - if (top.Model is BeatmapInfo && bottom.Model is BeatmapSetInfo) + if (top.Model is BeatmapInfo && bottom.Model is BeatmapSetUnderGrouping) return SPACING * 2; // Beatmap difficulty panels do not overlap with themselves or any other panel. @@ -206,12 +207,12 @@ namespace osu.Game.Screens.SelectV2 return true; } - if (item.Model is BeatmapSetInfo beatmapSetInfo) + if (item.Model is BeatmapSetUnderGrouping setUnderGrouping) { - if (oldItems.Contains(beatmapSetInfo)) + if (oldItems.Contains(setUnderGrouping.BeatmapSet)) return false; - RequestRecommendedSelection(beatmapSetInfo.Beatmaps); + RequestRecommendedSelection(setUnderGrouping.BeatmapSet.Beatmaps); return true; } } @@ -282,7 +283,7 @@ namespace osu.Game.Screens.SelectV2 protected GroupDefinition? ExpandedGroup { get; private set; } - protected BeatmapSetInfo? ExpandedBeatmapSet { get; private set; } + protected BeatmapSetUnderGrouping? ExpandedBeatmapSet { get; private set; } protected override bool ShouldActivateOnKeyboardSelection(CarouselItem item) => grouping.BeatmapSetsGroupedTogether && item.Model is BeatmapInfo; @@ -310,8 +311,8 @@ namespace osu.Game.Screens.SelectV2 return; - case BeatmapSetInfo setInfo: - selectRecommendedDifficultyForBeatmapSet(setInfo); + case BeatmapSetUnderGrouping setUnderGrouping: + selectRecommendedDifficultyForBeatmapSet(setUnderGrouping); return; case BeatmapInfo beatmapInfo: @@ -337,7 +338,7 @@ namespace osu.Game.Screens.SelectV2 switch (model) { - case BeatmapSetInfo: + case BeatmapSetUnderGrouping: case GroupDefinition: throw new InvalidOperationException("Groups should never become selected"); @@ -348,7 +349,7 @@ namespace osu.Game.Screens.SelectV2 setExpandedGroup(containingGroup); if (grouping.BeatmapSetsGroupedTogether) - setExpandedSet(beatmapInfo); + setExpandedSet(new BeatmapSetUnderGrouping(containingGroup, beatmapInfo.BeatmapSet!)); break; } } @@ -372,10 +373,10 @@ namespace osu.Game.Screens.SelectV2 setExpandedGroup(groupForReselection); } - private void selectRecommendedDifficultyForBeatmapSet(BeatmapSetInfo beatmapSet) + private void selectRecommendedDifficultyForBeatmapSet(BeatmapSetUnderGrouping setUnderGrouping) { // Selecting a set isn't valid – let's re-select the first visible difficulty. - if (grouping.SetItems.TryGetValue(beatmapSet, out var items)) + if (grouping.SetItems.TryGetValue(setUnderGrouping, out var items)) { var beatmaps = items.Select(i => i.Model).OfType(); RequestRecommendedSelection(beatmaps); @@ -423,7 +424,7 @@ namespace osu.Game.Screens.SelectV2 { switch (item.Model) { - case BeatmapSetInfo: + case BeatmapSetUnderGrouping: return true; case BeatmapInfo: @@ -462,11 +463,11 @@ namespace osu.Game.Screens.SelectV2 i.IsExpanded = true; break; - case BeatmapSetInfo set: + case BeatmapSetUnderGrouping setUnderGrouping: // Case where there are set headers, header should be visible // and items should use the set's expanded state. i.IsVisible = true; - setExpansionStateOfSetItems(set, i.IsExpanded); + setExpansionStateOfSetItems(setUnderGrouping, i.IsExpanded); break; default: @@ -496,21 +497,21 @@ namespace osu.Game.Screens.SelectV2 } } - private void setExpandedSet(BeatmapInfo beatmapInfo) + private void setExpandedSet(BeatmapSetUnderGrouping setUnderGrouping) { if (ExpandedBeatmapSet != null) setExpansionStateOfSetItems(ExpandedBeatmapSet, false); - ExpandedBeatmapSet = beatmapInfo.BeatmapSet!; + ExpandedBeatmapSet = setUnderGrouping; setExpansionStateOfSetItems(ExpandedBeatmapSet, true); } - private void setExpansionStateOfSetItems(BeatmapSetInfo set, bool expanded) + private void setExpansionStateOfSetItems(BeatmapSetUnderGrouping set, bool expanded) { if (grouping.SetItems.TryGetValue(set, out var items)) { foreach (var i in items) { - if (i.Model is BeatmapSetInfo) + if (i.Model is BeatmapSetUnderGrouping) i.IsExpanded = expanded; else i.IsVisible = expanded; @@ -548,7 +549,7 @@ namespace osu.Game.Screens.SelectV2 sampleToggleGroup?.Play(); return; - case BeatmapSetInfo: + case BeatmapSetUnderGrouping: sampleChangeSet?.Play(); return; @@ -687,8 +688,8 @@ namespace osu.Game.Screens.SelectV2 // it is doing a Replace operation on the list. If it is, then check the local handling in beatmapSetsChanged // before changing matching requirements here. - if (x is BeatmapSetInfo beatmapSetX && y is BeatmapSetInfo beatmapSetY) - return beatmapSetX.Equals(beatmapSetY); + if (x is BeatmapSetUnderGrouping setUnderGroupingX && y is BeatmapSetUnderGrouping setUnderGroupingY) + return setUnderGroupingX.Equals(setUnderGroupingY); if (x is BeatmapInfo beatmapX && y is BeatmapInfo beatmapY) return beatmapX.Equals(beatmapY); @@ -718,7 +719,7 @@ namespace osu.Game.Screens.SelectV2 return beatmapPanelPool.Get(); - case BeatmapSetInfo: + case BeatmapSetUnderGrouping: return setPanelPool.Get(); } @@ -828,30 +829,31 @@ namespace osu.Game.Screens.SelectV2 private bool nextRandomSet() { - ICollection visibleSets = ExpandedGroup != null + ICollection visibleSetsUnderGrouping = ExpandedGroup != null // In the case of grouping, users expect random to only operate on the expanded group. // This is going to incur some overhead as we don't have a group-beatmapset mapping currently. // // If this becomes an issue, we could either store a mapping, or run the random algorithm many times // using the `SetItems` method until we get a group HIT. - ? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().ToArray() + ? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().ToArray() // This is the fastest way to retrieve sets for randomisation. : grouping.SetItems.Keys; - BeatmapSetInfo set; + BeatmapSetUnderGrouping set; switch (randomAlgorithm.Value) { case RandomSelectAlgorithm.RandomPermutation: { - ICollection notYetVisitedSets = visibleSets.Except(previouslyVisitedRandomBeatmaps.Select(b => b.BeatmapSet!)).ToList(); + ICollection notYetVisitedSets = + visibleSetsUnderGrouping.ExceptBy(previouslyVisitedRandomBeatmaps.Select(b => b.BeatmapSet!), setUnderGrouping => setUnderGrouping.BeatmapSet).ToList(); if (!notYetVisitedSets.Any()) { - previouslyVisitedRandomBeatmaps.RemoveAll(b => visibleSets.Contains(b.BeatmapSet!)); - notYetVisitedSets = visibleSets; + previouslyVisitedRandomBeatmaps.RemoveAll(b => visibleSetsUnderGrouping.Any(setUnderGrouping => setUnderGrouping.BeatmapSet.Equals(b.BeatmapSet!))); + notYetVisitedSets = visibleSetsUnderGrouping; if (CurrentSelection is BeatmapInfo beatmapInfo) - notYetVisitedSets = notYetVisitedSets.Except([beatmapInfo.BeatmapSet!]).ToList(); + notYetVisitedSets = notYetVisitedSets.ExceptBy([beatmapInfo.BeatmapSet!], setUnderGrouping => setUnderGrouping.BeatmapSet).ToList(); } if (notYetVisitedSets.Count == 0) @@ -862,7 +864,7 @@ namespace osu.Game.Screens.SelectV2 } case RandomSelectAlgorithm.Random: - set = visibleSets.ElementAt(RNG.Next(visibleSets.Count)); + set = visibleSetsUnderGrouping.ElementAt(RNG.Next(visibleSetsUnderGrouping.Count)); break; default: @@ -959,4 +961,10 @@ namespace osu.Game.Screens.SelectV2 /// Defines a grouping header for a set of carousel items grouped by star difficulty. /// public record StarDifficultyGroupDefinition(int Order, string Title, StarDifficulty Difficulty) : GroupDefinition(Order, Title); + + /// + /// Used to represent a portion of a under a . + /// The purpose of this model is to support splitting beatmap sets apart when the active grouping mode demands it. + /// + public record BeatmapSetUnderGrouping([UsedImplicitly] GroupDefinition? Group, BeatmapSetInfo BeatmapSet); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index f0ec3ae3ab..63bc94b087 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -29,14 +29,14 @@ namespace osu.Game.Screens.SelectV2 /// /// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection. /// - public IDictionary> SetItems => setMap; + public IDictionary> SetItems => setMap; /// /// Groups contain children which are group-selectable. This dictionary holds the relationships between groups-panels to allow expanding them on selection. /// public IDictionary> GroupItems => groupMap; - private Dictionary> setMap = new Dictionary>(); + private Dictionary> setMap = new Dictionary>(); private Dictionary> groupMap = new Dictionary>(); private readonly Func getCriteria; @@ -56,7 +56,7 @@ namespace osu.Game.Screens.SelectV2 return await Task.Run(() => { // preallocate space for the new mappings using last known estimates - var newSetMap = new Dictionary>(setMap.Count); + var newSetMap = new Dictionary>(setMap.Count); var newGroupMap = new Dictionary>(groupMap.Count); var criteria = getCriteria(); @@ -94,11 +94,12 @@ namespace osu.Game.Screens.SelectV2 var beatmap = (BeatmapInfo)item.Model; bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; + var beatmapSetUnderGrouping = new BeatmapSetUnderGrouping(group, beatmap.BeatmapSet!); if (newBeatmapSet) { - if (!newSetMap.TryGetValue(beatmap.BeatmapSet!, out currentSetItems)) - newSetMap[beatmap.BeatmapSet!] = currentSetItems = new HashSet(); + if (!newSetMap.TryGetValue(beatmapSetUnderGrouping, out currentSetItems)) + newSetMap[beatmapSetUnderGrouping] = currentSetItems = new HashSet(); } if (BeatmapSetsGroupedTogether) @@ -108,7 +109,7 @@ namespace osu.Game.Screens.SelectV2 if (groupItem != null) groupItem.NestedItemCount++; - addItem(new CarouselItem(beatmap.BeatmapSet!) + addItem(new CarouselItem(beatmapSetUnderGrouping) { DrawHeight = PanelBeatmapSet.HEIGHT, DepthLayer = -1 @@ -135,7 +136,7 @@ namespace osu.Game.Screens.SelectV2 currentGroupItems?.Add(i); currentSetItems?.Add(i); - i.IsVisible = i.Model is GroupDefinition || (group == null && (i.Model is BeatmapSetInfo || !BeatmapSetsGroupedTogether)); + i.IsVisible = i.Model is GroupDefinition || (group == null && (i.Model is BeatmapSetUnderGrouping || !BeatmapSetsGroupedTogether)); } } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index d776ab1ffb..7b07076975 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -67,6 +67,15 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IBindable ruleset { get; set; } = null!; + private BeatmapSetUnderGrouping beatmapSetUnderGrouping + { + get + { + Debug.Assert(Item != null); + return (BeatmapSetUnderGrouping)Item!.Model; + } + } + public PanelBeatmapSet() { PanelXOffset = 20f; @@ -179,9 +188,7 @@ namespace osu.Game.Screens.SelectV2 { base.PrepareForUse(); - Debug.Assert(Item != null); - - var beatmapSet = (BeatmapSetInfo)Item.Model; + var beatmapSet = beatmapSetUnderGrouping.BeatmapSet; // Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set). setBackground.Beatmap = beatmaps.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID)); @@ -215,7 +222,7 @@ namespace osu.Game.Screens.SelectV2 if (Item == null) return Array.Empty(); - var beatmapSet = (BeatmapSetInfo)Item.Model; + var beatmapSet = beatmapSetUnderGrouping.BeatmapSet; List items = new List(); @@ -268,9 +275,7 @@ namespace osu.Game.Screens.SelectV2 private MenuItem createCollectionMenuItem(BeatmapCollection collection) { - var beatmapSet = (BeatmapSetInfo)Item!.Model; - - Debug.Assert(beatmapSet != null); + var beatmapSet = beatmapSetUnderGrouping.BeatmapSet; TernaryState state; From 6ba72fa481462932cf770781a2ac28263fd3e4e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Aug 2025 10:35:31 +0200 Subject: [PATCH 47/57] Adjust tests to new beatmap set model usage in carousel --- .../Visual/Navigation/TestScenePresentBeatmap.cs | 2 +- .../BeatmapCarouselFilterGroupingTest.cs | 2 +- .../SongSelectV2/BeatmapCarouselTestScene.cs | 2 +- .../TestSceneBeatmapCarouselFiltering.cs | 16 ++++++++++++---- .../Visual/SongSelectV2/TestScenePanelSet.cs | 8 ++++---- .../SongSelectV2/TestSceneSongSelectGrouping.cs | 2 +- 6 files changed, 20 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index e7172cacbf..6092bdde3a 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -181,7 +181,7 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("beatmap in song select", () => { var songSelect = (SoloSongSelect)Game.ScreenStack.CurrentScreen; - return songSelect.ChildrenOfType().Single().GetCarouselItems()!.Any(i => i.Model is BeatmapSetInfo bsi && bsi.MatchesOnlineID(getImport())); + return songSelect.ChildrenOfType().Single().GetCarouselItems()!.Any(i => i.Model is BeatmapSetUnderGrouping bsug && bsug.BeatmapSet.MatchesOnlineID(getImport())); }); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs index 592994f2f0..efd4eb7b03 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ]; var results = await runGrouping(GroupMode.None, beatmapSets); - Assert.That(results.Select(r => r.Model).OfType(), Is.EquivalentTo(beatmapSets)); + Assert.That(results.Select(r => r.Model).OfType().Select(setUnderGrouping => setUnderGrouping.BeatmapSet), Is.EquivalentTo(beatmapSets)); Assert.That(results.Select(r => r.Model).OfType(), Is.EquivalentTo(allBeatmaps)); assertTotal(results, beatmapSets.Count + allBeatmaps.Length); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 64084d76f1..2664062fc2 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -237,7 +237,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // Using groupingFilter.SetItems.Count alone doesn't work. // When sorting by difficulty, there can be more than one set panel for the same set displayed. - return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is BeatmapSetInfo)); + return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is BeatmapSetUnderGrouping)); }, () => Is.EqualTo(expected)); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs index 78c12e2730..d599c07f27 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs @@ -396,7 +396,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilterAndWaitForFilter("filter first away", c => c.UserStarDifficulty.Min = 3); SelectNextPanel(); - AddAssert("keyboard selected is first set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.First())); + AddAssert("keyboard selected is first set", + () => (GetKeyboardSelectedPanel()?.Item?.Model as BeatmapSetUnderGrouping)?.BeatmapSet, + () => Is.EqualTo(BeatmapSets.First())); } [Test] @@ -413,7 +415,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilterAndWaitForFilter("filter first away", c => c.UserStarDifficulty.Min = 3); SelectPrevPanel(); - AddAssert("keyboard selected is last set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.Last())); + AddAssert("keyboard selected is last set", + () => (GetKeyboardSelectedPanel()?.Item?.Model as BeatmapSetUnderGrouping)?.BeatmapSet, + () => Is.EqualTo(BeatmapSets.Last())); } [Test] @@ -428,7 +432,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilterAndWaitForFilter("filter last set away", c => c.SearchText = BeatmapSets.First().Metadata.Title); SelectPrevPanel(); - AddAssert("keyboard selected is first set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.First())); + AddAssert("keyboard selected is first set", + () => (GetKeyboardSelectedPanel()?.Item?.Model as BeatmapSetUnderGrouping)?.BeatmapSet, + () => Is.EqualTo(BeatmapSets.First())); } [Test] @@ -444,7 +450,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // Single result is automatically selected for us, so we iterate once backwards to the set header. SelectPrevPanel(); - AddAssert("keyboard selected is second set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.Last())); + AddAssert("keyboard selected is second set", + () => (GetKeyboardSelectedPanel()?.Item?.Model as BeatmapSetUnderGrouping)?.BeatmapSet, + () => Is.EqualTo(BeatmapSets.Last())); } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs index 1723185b1f..6a212381a8 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs @@ -75,21 +75,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { new PanelBeatmapSet { - Item = new CarouselItem(beatmapSet) + Item = new CarouselItem(new BeatmapSetUnderGrouping(null, beatmapSet)) }, new PanelBeatmapSet { - Item = new CarouselItem(beatmapSet), + Item = new CarouselItem(new BeatmapSetUnderGrouping(null, beatmapSet)), KeyboardSelected = { Value = true } }, new PanelBeatmapSet { - Item = new CarouselItem(beatmapSet), + Item = new CarouselItem(new BeatmapSetUnderGrouping(null, beatmapSet)), Expanded = { Value = true } }, new PanelBeatmapSet { - Item = new CarouselItem(beatmapSet), + Item = new CarouselItem(new BeatmapSetUnderGrouping(null, beatmapSet)), KeyboardSelected = { Value = true }, Expanded = { Value = true } }, diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs index 0f7c42946d..be7f705532 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("no-collection group present", () => { var group = grouping.GroupItems.Single(g => g.Key.Title == "Not in collection"); - return group.Value.Select(i => i.Model).OfType().Single().Equals(beatmapSet); + return group.Value.Select(i => i.Model).OfType().Single().BeatmapSet.Equals(beatmapSet); }); AddStep("add beatmap to collection", () => From bb9f9e4d358461d471c682a6af1d83e028523a41 Mon Sep 17 00:00:00 2001 From: marvin Date: Thu, 28 Aug 2025 23:34:22 +0200 Subject: [PATCH 48/57] Fix operations in PooledDrawableWithLifetimeContainer.CheckChildrenLife being in wrong order Previously CompositeDrawable.CheckChildrenLife() would be run before lifetimeManager.Update() which lead to the new drawables being inserted into the container but not being made alive immediately, leading to the drawable not becoming visibile until the next update loop. --- .../Objects/Pooling/PooledDrawableWithLifetimeContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs index efc10f26e1..e01df1428c 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs @@ -160,8 +160,8 @@ namespace osu.Game.Rulesets.Objects.Pooling if (!IsPresent) return false; - bool aliveChanged = base.CheckChildrenLife(); - aliveChanged |= lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); + bool aliveChanged = lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); + aliveChanged |= base.CheckChildrenLife(); return aliveChanged; } } From f2f5cf19a286821e46ff609f1394c66a485d879e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Aug 2025 14:08:06 +0900 Subject: [PATCH 49/57] Return early to avoid creating mod description strings unnecessarily --- osu.Game/Rulesets/Mods/Mod.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 727db913e2..628098c5b6 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -56,6 +56,9 @@ namespace osu.Game.Rulesets.Mods { var bindable = (IBindable)property.GetValue(this)!; + if (!bindable.IsDefault) + continue; + string valueText; switch (bindable) @@ -69,8 +72,7 @@ namespace osu.Game.Rulesets.Mods break; } - if (!bindable.IsDefault) - yield return (attr.Label, valueText); + yield return (attr.Label, valueText); } } } From e83f3d5e778397b5fbb6fb778209afb70521e70b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Aug 2025 14:08:18 +0900 Subject: [PATCH 50/57] Fix some mods showing tooltips when settings are default --- osu.Game/Rulesets/Mods/ModBarrelRoll.cs | 9 ++++++--- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs index 22d2f41b82..98a7999065 100644 --- a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs +++ b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs @@ -46,8 +46,10 @@ namespace osu.Game.Rulesets.Mods { get { - yield return ("Roll speed", $"{SpinSpeed.Value:N2} rpm"); - yield return ("Direction", Direction.Value.GetDescription()); + if (!SpinSpeed.IsDefault) + yield return ("Roll speed", $"{SpinSpeed.Value:N2} rpm"); + if (!Direction.IsDefault) + yield return ("Direction", Direction.Value.GetDescription()); } } @@ -55,7 +57,8 @@ namespace osu.Game.Rulesets.Mods public virtual void Update(Playfield playfield) { - playfieldAdjustmentContainer.Rotation = CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value); + playfieldAdjustmentContainer.Rotation = + CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value); } public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 8dfe8444e8..049b8f9b7f 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -41,7 +41,8 @@ namespace osu.Game.Rulesets.Mods { get { - yield return ("Speed change", $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"); + if (!InitialRate.IsDefault || !FinalRate.IsDefault) + yield return ("Speed change", $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"); if (!AdjustPitch.IsDefault) yield return ("Adjust pitch", AdjustPitch.Value ? "On" : "Off"); From 12832e9fef04daf0e87e6ec9cada56d0d056bfbb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Aug 2025 14:35:42 +0900 Subject: [PATCH 51/57] Use switches for warmup/chat toggles in tournament interface As proposed in https://github.com/ppy/osu/discussions/32515. --- .../Screens/Gameplay/GameplayScreen.cs | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index b2152eaf3d..2cf7ce1961 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -7,7 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Threading; -using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays.Settings; using osu.Game.Tournament.Components; using osu.Game.Tournament.IPC; @@ -24,7 +24,6 @@ namespace osu.Game.Tournament.Screens.Gameplay private readonly BindableBool warmup = new BindableBool(); public readonly Bindable State = new Bindable(); - private OsuButton warmupButton = null!; private MatchIPCInfo ipc = null!; [Resolved] @@ -40,6 +39,8 @@ namespace osu.Game.Tournament.Screens.Gameplay { this.ipc = ipc; + LabelledSwitchButton chatToggle; + AddRangeInternal(new Drawable[] { new TourneyVideo("gameplay") @@ -95,17 +96,14 @@ namespace osu.Game.Tournament.Screens.Gameplay { Children = new Drawable[] { - warmupButton = new TourneyButton + new LabelledSwitchButton { - RelativeSizeAxes = Axes.X, - Text = "Toggle warmup", - Action = () => warmup.Toggle() + Label = "Warmup", + Current = warmup, }, - new TourneyButton + chatToggle = new LabelledSwitchButton { - RelativeSizeAxes = Axes.X, - Text = "Toggle chat", - Action = () => { State.Value = State.Value == TourneyState.Idle ? TourneyState.Playing : TourneyState.Idle; } + Label = "Show chat", }, new SettingsSlider { @@ -123,13 +121,12 @@ namespace osu.Game.Tournament.Screens.Gameplay } }); + State.BindValueChanged(state => chatToggle.Current.Value = State.Value == TourneyState.Idle, true); + chatToggle.Current.BindValueChanged(v => State.Value = v.NewValue ? TourneyState.Idle : TourneyState.Playing); + LadderInfo.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true); - warmup.BindValueChanged(w => - { - warmupButton.Alpha = !w.NewValue ? 0.5f : 1; - header.ShowScores = !w.NewValue; - }, true); + warmup.BindValueChanged(w => header.ShowScores = !w.NewValue, true); } protected override void LoadComplete() From 9e77a5b0507c7d71fad374c9d59169dbe0ece269 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Aug 2025 16:01:49 +0900 Subject: [PATCH 52/57] Fix obviously incorrect conditional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Rulesets/Mods/Mod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 628098c5b6..477372b97d 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mods { var bindable = (IBindable)property.GetValue(this)!; - if (!bindable.IsDefault) + if (bindable.IsDefault) continue; string valueText; From df6d6edaca6e69736ddd52ff78cf026717aae935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Aug 2025 09:29:47 +0200 Subject: [PATCH 53/57] Fix song select not performing online lookup on re-enter Closes https://github.com/ppy/osu/issues/34825. Root cause is https://github.com/ppy/osu/blob/24ec43b3b65fa3b164b7713341cd62b1e0dacc2e/osu.Game/Screens/SelectV2/SongSelect.cs#L345-L356 not specifying `(..., true)`, therefore the fetch doesn't happen on enter if song select doesn't change the global beatmap as a side effect of the enter, which is the case on re-entering. --- osu.Game/Screens/SelectV2/SongSelect.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 947b8f9c7c..ef00064ced 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -653,6 +653,7 @@ namespace osu.Game.Screens.SelectV2 ensurePlayingSelected(); updateBackgroundDim(); + fetchOnlineInfo(); } private void onLeavingScreen() From 526ee32268fd74a65ebe42fe53bcdb9cfe32fe12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Aug 2025 09:54:42 +0200 Subject: [PATCH 54/57] Apply suggested rename --- .../Navigation/TestScenePresentBeatmap.cs | 2 +- .../BeatmapCarouselFilterGroupingTest.cs | 2 +- .../SongSelectV2/BeatmapCarouselTestScene.cs | 4 +- .../TestSceneBeatmapCarouselFiltering.cs | 8 +-- .../Visual/SongSelectV2/TestScenePanelSet.cs | 8 +-- .../TestSceneSongSelectGrouping.cs | 2 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 62 +++++++++---------- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 16 ++--- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 10 +-- 9 files changed, 57 insertions(+), 57 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index 6092bdde3a..1dd39e5bf9 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -181,7 +181,7 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("beatmap in song select", () => { var songSelect = (SoloSongSelect)Game.ScreenStack.CurrentScreen; - return songSelect.ChildrenOfType().Single().GetCarouselItems()!.Any(i => i.Model is BeatmapSetUnderGrouping bsug && bsug.BeatmapSet.MatchesOnlineID(getImport())); + return songSelect.ChildrenOfType().Single().GetCarouselItems()!.Any(i => i.Model is GroupedBeatmapSet gbs && gbs.BeatmapSet.MatchesOnlineID(getImport())); }); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs index efd4eb7b03..32a7b89424 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ]; var results = await runGrouping(GroupMode.None, beatmapSets); - Assert.That(results.Select(r => r.Model).OfType().Select(setUnderGrouping => setUnderGrouping.BeatmapSet), Is.EquivalentTo(beatmapSets)); + Assert.That(results.Select(r => r.Model).OfType().Select(groupedSet => groupedSet.BeatmapSet), Is.EquivalentTo(beatmapSets)); Assert.That(results.Select(r => r.Model).OfType(), Is.EquivalentTo(allBeatmaps)); assertTotal(results, beatmapSets.Count + allBeatmaps.Length); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 2664062fc2..f18e1e9b52 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -237,7 +237,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // Using groupingFilter.SetItems.Count alone doesn't work. // When sorting by difficulty, there can be more than one set panel for the same set displayed. - return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is BeatmapSetUnderGrouping)); + return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is GroupedBeatmapSet)); }, () => Is.EqualTo(expected)); } @@ -440,7 +440,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 public BeatmapInfo? SelectedBeatmapInfo => CurrentSelection as BeatmapInfo; public BeatmapSetInfo? SelectedBeatmapSet => SelectedBeatmapInfo?.BeatmapSet; - public new BeatmapSetUnderGrouping? ExpandedBeatmapSet => base.ExpandedBeatmapSet; + public new GroupedBeatmapSet? ExpandedBeatmapSet => base.ExpandedBeatmapSet; public new GroupDefinition? ExpandedGroup => base.ExpandedGroup; public TestBeatmapCarousel() diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs index d599c07f27..687c4c23be 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs @@ -397,7 +397,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextPanel(); AddAssert("keyboard selected is first set", - () => (GetKeyboardSelectedPanel()?.Item?.Model as BeatmapSetUnderGrouping)?.BeatmapSet, + () => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet, () => Is.EqualTo(BeatmapSets.First())); } @@ -416,7 +416,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectPrevPanel(); AddAssert("keyboard selected is last set", - () => (GetKeyboardSelectedPanel()?.Item?.Model as BeatmapSetUnderGrouping)?.BeatmapSet, + () => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet, () => Is.EqualTo(BeatmapSets.Last())); } @@ -433,7 +433,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectPrevPanel(); AddAssert("keyboard selected is first set", - () => (GetKeyboardSelectedPanel()?.Item?.Model as BeatmapSetUnderGrouping)?.BeatmapSet, + () => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet, () => Is.EqualTo(BeatmapSets.First())); } @@ -451,7 +451,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // Single result is automatically selected for us, so we iterate once backwards to the set header. SelectPrevPanel(); AddAssert("keyboard selected is second set", - () => (GetKeyboardSelectedPanel()?.Item?.Model as BeatmapSetUnderGrouping)?.BeatmapSet, + () => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet, () => Is.EqualTo(BeatmapSets.Last())); } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs index 6a212381a8..b574262d55 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs @@ -75,21 +75,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { new PanelBeatmapSet { - Item = new CarouselItem(new BeatmapSetUnderGrouping(null, beatmapSet)) + Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet)) }, new PanelBeatmapSet { - Item = new CarouselItem(new BeatmapSetUnderGrouping(null, beatmapSet)), + Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet)), KeyboardSelected = { Value = true } }, new PanelBeatmapSet { - Item = new CarouselItem(new BeatmapSetUnderGrouping(null, beatmapSet)), + Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet)), Expanded = { Value = true } }, new PanelBeatmapSet { - Item = new CarouselItem(new BeatmapSetUnderGrouping(null, beatmapSet)), + Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet)), KeyboardSelected = { Value = true }, Expanded = { Value = true } }, diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs index be7f705532..0772607a57 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("no-collection group present", () => { var group = grouping.GroupItems.Single(g => g.Key.Title == "Not in collection"); - return group.Value.Select(i => i.Model).OfType().Single().BeatmapSet.Equals(beatmapSet); + return group.Value.Select(i => i.Model).OfType().Single().BeatmapSet.Equals(beatmapSet); }); AddStep("add beatmap to collection", () => diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 95fb26c6dd..22079ea91f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -70,11 +70,11 @@ namespace osu.Game.Screens.SelectV2 if (grouping.BeatmapSetsGroupedTogether) { // Give some space around the expanded beatmap set, at the top.. - if (bottom.Model is BeatmapSetUnderGrouping && bottom.IsExpanded) + if (bottom.Model is GroupedBeatmapSet && bottom.IsExpanded) return SPACING * 2; // ..and the bottom. - if (top.Model is BeatmapInfo && bottom.Model is BeatmapSetUnderGrouping) + if (top.Model is BeatmapInfo && bottom.Model is GroupedBeatmapSet) return SPACING * 2; // Beatmap difficulty panels do not overlap with themselves or any other panel. @@ -207,12 +207,12 @@ namespace osu.Game.Screens.SelectV2 return true; } - if (item.Model is BeatmapSetUnderGrouping setUnderGrouping) + if (item.Model is GroupedBeatmapSet groupedSet) { - if (oldItems.Contains(setUnderGrouping.BeatmapSet)) + if (oldItems.Contains(groupedSet.BeatmapSet)) return false; - RequestRecommendedSelection(setUnderGrouping.BeatmapSet.Beatmaps); + RequestRecommendedSelection(groupedSet.BeatmapSet.Beatmaps); return true; } } @@ -283,7 +283,7 @@ namespace osu.Game.Screens.SelectV2 protected GroupDefinition? ExpandedGroup { get; private set; } - protected BeatmapSetUnderGrouping? ExpandedBeatmapSet { get; private set; } + protected GroupedBeatmapSet? ExpandedBeatmapSet { get; private set; } protected override bool ShouldActivateOnKeyboardSelection(CarouselItem item) => grouping.BeatmapSetsGroupedTogether && item.Model is BeatmapInfo; @@ -311,8 +311,8 @@ namespace osu.Game.Screens.SelectV2 return; - case BeatmapSetUnderGrouping setUnderGrouping: - selectRecommendedDifficultyForBeatmapSet(setUnderGrouping); + case GroupedBeatmapSet groupedSet: + selectRecommendedDifficultyForBeatmapSet(groupedSet); return; case BeatmapInfo beatmapInfo: @@ -338,7 +338,7 @@ namespace osu.Game.Screens.SelectV2 switch (model) { - case BeatmapSetUnderGrouping: + case GroupedBeatmapSet: case GroupDefinition: throw new InvalidOperationException("Groups should never become selected"); @@ -349,7 +349,7 @@ namespace osu.Game.Screens.SelectV2 setExpandedGroup(containingGroup); if (grouping.BeatmapSetsGroupedTogether) - setExpandedSet(new BeatmapSetUnderGrouping(containingGroup, beatmapInfo.BeatmapSet!)); + setExpandedSet(new GroupedBeatmapSet(containingGroup, beatmapInfo.BeatmapSet!)); break; } } @@ -373,10 +373,10 @@ namespace osu.Game.Screens.SelectV2 setExpandedGroup(groupForReselection); } - private void selectRecommendedDifficultyForBeatmapSet(BeatmapSetUnderGrouping setUnderGrouping) + private void selectRecommendedDifficultyForBeatmapSet(GroupedBeatmapSet set) { // Selecting a set isn't valid – let's re-select the first visible difficulty. - if (grouping.SetItems.TryGetValue(setUnderGrouping, out var items)) + if (grouping.SetItems.TryGetValue(set, out var items)) { var beatmaps = items.Select(i => i.Model).OfType(); RequestRecommendedSelection(beatmaps); @@ -424,7 +424,7 @@ namespace osu.Game.Screens.SelectV2 { switch (item.Model) { - case BeatmapSetUnderGrouping: + case GroupedBeatmapSet: return true; case BeatmapInfo: @@ -463,11 +463,11 @@ namespace osu.Game.Screens.SelectV2 i.IsExpanded = true; break; - case BeatmapSetUnderGrouping setUnderGrouping: + case GroupedBeatmapSet groupedSet: // Case where there are set headers, header should be visible // and items should use the set's expanded state. i.IsVisible = true; - setExpansionStateOfSetItems(setUnderGrouping, i.IsExpanded); + setExpansionStateOfSetItems(groupedSet, i.IsExpanded); break; default: @@ -497,21 +497,21 @@ namespace osu.Game.Screens.SelectV2 } } - private void setExpandedSet(BeatmapSetUnderGrouping setUnderGrouping) + private void setExpandedSet(GroupedBeatmapSet set) { if (ExpandedBeatmapSet != null) setExpansionStateOfSetItems(ExpandedBeatmapSet, false); - ExpandedBeatmapSet = setUnderGrouping; + ExpandedBeatmapSet = set; setExpansionStateOfSetItems(ExpandedBeatmapSet, true); } - private void setExpansionStateOfSetItems(BeatmapSetUnderGrouping set, bool expanded) + private void setExpansionStateOfSetItems(GroupedBeatmapSet set, bool expanded) { if (grouping.SetItems.TryGetValue(set, out var items)) { foreach (var i in items) { - if (i.Model is BeatmapSetUnderGrouping) + if (i.Model is GroupedBeatmapSet) i.IsExpanded = expanded; else i.IsVisible = expanded; @@ -549,7 +549,7 @@ namespace osu.Game.Screens.SelectV2 sampleToggleGroup?.Play(); return; - case BeatmapSetUnderGrouping: + case GroupedBeatmapSet: sampleChangeSet?.Play(); return; @@ -688,8 +688,8 @@ namespace osu.Game.Screens.SelectV2 // it is doing a Replace operation on the list. If it is, then check the local handling in beatmapSetsChanged // before changing matching requirements here. - if (x is BeatmapSetUnderGrouping setUnderGroupingX && y is BeatmapSetUnderGrouping setUnderGroupingY) - return setUnderGroupingX.Equals(setUnderGroupingY); + if (x is GroupedBeatmapSet groupedSetX && y is GroupedBeatmapSet groupedSetY) + return groupedSetX.Equals(groupedSetY); if (x is BeatmapInfo beatmapX && y is BeatmapInfo beatmapY) return beatmapX.Equals(beatmapY); @@ -719,7 +719,7 @@ namespace osu.Game.Screens.SelectV2 return beatmapPanelPool.Get(); - case BeatmapSetUnderGrouping: + case GroupedBeatmapSet: return setPanelPool.Get(); } @@ -829,31 +829,31 @@ namespace osu.Game.Screens.SelectV2 private bool nextRandomSet() { - ICollection visibleSetsUnderGrouping = ExpandedGroup != null + ICollection visibleSetsUnderGrouping = ExpandedGroup != null // In the case of grouping, users expect random to only operate on the expanded group. // This is going to incur some overhead as we don't have a group-beatmapset mapping currently. // // If this becomes an issue, we could either store a mapping, or run the random algorithm many times // using the `SetItems` method until we get a group HIT. - ? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().ToArray() + ? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().ToArray() // This is the fastest way to retrieve sets for randomisation. : grouping.SetItems.Keys; - BeatmapSetUnderGrouping set; + GroupedBeatmapSet set; switch (randomAlgorithm.Value) { case RandomSelectAlgorithm.RandomPermutation: { - ICollection notYetVisitedSets = - visibleSetsUnderGrouping.ExceptBy(previouslyVisitedRandomBeatmaps.Select(b => b.BeatmapSet!), setUnderGrouping => setUnderGrouping.BeatmapSet).ToList(); + ICollection notYetVisitedSets = + visibleSetsUnderGrouping.ExceptBy(previouslyVisitedRandomBeatmaps.Select(b => b.BeatmapSet!), groupedSet => groupedSet.BeatmapSet).ToList(); if (!notYetVisitedSets.Any()) { - previouslyVisitedRandomBeatmaps.RemoveAll(b => visibleSetsUnderGrouping.Any(setUnderGrouping => setUnderGrouping.BeatmapSet.Equals(b.BeatmapSet!))); + previouslyVisitedRandomBeatmaps.RemoveAll(b => visibleSetsUnderGrouping.Any(groupedSet => groupedSet.BeatmapSet.Equals(b.BeatmapSet!))); notYetVisitedSets = visibleSetsUnderGrouping; if (CurrentSelection is BeatmapInfo beatmapInfo) - notYetVisitedSets = notYetVisitedSets.ExceptBy([beatmapInfo.BeatmapSet!], setUnderGrouping => setUnderGrouping.BeatmapSet).ToList(); + notYetVisitedSets = notYetVisitedSets.ExceptBy([beatmapInfo.BeatmapSet!], groupedSet => groupedSet.BeatmapSet).ToList(); } if (notYetVisitedSets.Count == 0) @@ -966,5 +966,5 @@ namespace osu.Game.Screens.SelectV2 /// Used to represent a portion of a under a . /// The purpose of this model is to support splitting beatmap sets apart when the active grouping mode demands it. /// - public record BeatmapSetUnderGrouping([UsedImplicitly] GroupDefinition? Group, BeatmapSetInfo BeatmapSet); + public record GroupedBeatmapSet([UsedImplicitly] GroupDefinition? Group, BeatmapSetInfo BeatmapSet); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 63bc94b087..0d2489c304 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -29,14 +29,14 @@ namespace osu.Game.Screens.SelectV2 /// /// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection. /// - public IDictionary> SetItems => setMap; + public IDictionary> SetItems => setMap; /// /// Groups contain children which are group-selectable. This dictionary holds the relationships between groups-panels to allow expanding them on selection. /// public IDictionary> GroupItems => groupMap; - private Dictionary> setMap = new Dictionary>(); + private Dictionary> setMap = new Dictionary>(); private Dictionary> groupMap = new Dictionary>(); private readonly Func getCriteria; @@ -56,7 +56,7 @@ namespace osu.Game.Screens.SelectV2 return await Task.Run(() => { // preallocate space for the new mappings using last known estimates - var newSetMap = new Dictionary>(setMap.Count); + var newSetMap = new Dictionary>(setMap.Count); var newGroupMap = new Dictionary>(groupMap.Count); var criteria = getCriteria(); @@ -94,12 +94,12 @@ namespace osu.Game.Screens.SelectV2 var beatmap = (BeatmapInfo)item.Model; bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; - var beatmapSetUnderGrouping = new BeatmapSetUnderGrouping(group, beatmap.BeatmapSet!); + var groupedBeatmapSet = new GroupedBeatmapSet(group, beatmap.BeatmapSet!); if (newBeatmapSet) { - if (!newSetMap.TryGetValue(beatmapSetUnderGrouping, out currentSetItems)) - newSetMap[beatmapSetUnderGrouping] = currentSetItems = new HashSet(); + if (!newSetMap.TryGetValue(groupedBeatmapSet, out currentSetItems)) + newSetMap[groupedBeatmapSet] = currentSetItems = new HashSet(); } if (BeatmapSetsGroupedTogether) @@ -109,7 +109,7 @@ namespace osu.Game.Screens.SelectV2 if (groupItem != null) groupItem.NestedItemCount++; - addItem(new CarouselItem(beatmapSetUnderGrouping) + addItem(new CarouselItem(groupedBeatmapSet) { DrawHeight = PanelBeatmapSet.HEIGHT, DepthLayer = -1 @@ -136,7 +136,7 @@ namespace osu.Game.Screens.SelectV2 currentGroupItems?.Add(i); currentSetItems?.Add(i); - i.IsVisible = i.Model is GroupDefinition || (group == null && (i.Model is BeatmapSetUnderGrouping || !BeatmapSetsGroupedTogether)); + i.IsVisible = i.Model is GroupDefinition || (group == null && (i.Model is GroupedBeatmapSet || !BeatmapSetsGroupedTogether)); } } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 7b07076975..1a6e886cb7 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -67,12 +67,12 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IBindable ruleset { get; set; } = null!; - private BeatmapSetUnderGrouping beatmapSetUnderGrouping + private GroupedBeatmapSet groupedBeatmapSet { get { Debug.Assert(Item != null); - return (BeatmapSetUnderGrouping)Item!.Model; + return (GroupedBeatmapSet)Item!.Model; } } @@ -188,7 +188,7 @@ namespace osu.Game.Screens.SelectV2 { base.PrepareForUse(); - var beatmapSet = beatmapSetUnderGrouping.BeatmapSet; + var beatmapSet = groupedBeatmapSet.BeatmapSet; // Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set). setBackground.Beatmap = beatmaps.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID)); @@ -222,7 +222,7 @@ namespace osu.Game.Screens.SelectV2 if (Item == null) return Array.Empty(); - var beatmapSet = beatmapSetUnderGrouping.BeatmapSet; + var beatmapSet = groupedBeatmapSet.BeatmapSet; List items = new List(); @@ -275,7 +275,7 @@ namespace osu.Game.Screens.SelectV2 private MenuItem createCollectionMenuItem(BeatmapCollection collection) { - var beatmapSet = beatmapSetUnderGrouping.BeatmapSet; + var beatmapSet = groupedBeatmapSet.BeatmapSet; TernaryState state; From 0a408a3ac4c1eec91f662bd04a297a4683eeb525 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Aug 2025 17:11:47 +0900 Subject: [PATCH 55/57] Fix tournament test failure due to control change --- osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index 31583bf8b7..eb9faa5930 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Tournament.Components; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Screens.Gameplay; @@ -66,6 +67,6 @@ namespace osu.Game.Tournament.Tests.Screens () => this.ChildrenOfType().All(score => score.Alpha == (visible ? 1 : 0))); private void toggleWarmup() - => AddStep("toggle warmup", () => this.ChildrenOfType().First().TriggerClick()); + => AddStep("toggle warmup", () => this.ChildrenOfType().First().ChildrenOfType().First().TriggerClick()); } } From 04ba5aa57538aaea6cdff7631a04a18839dca4df Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 29 Aug 2025 17:42:14 +0900 Subject: [PATCH 56/57] Move footer to ScreenTestScene --- .../SongSelectV2/SongSelectTestScene.cs | 50 ---------------- osu.Game/Tests/Visual/ScreenTestScene.cs | 60 +++++++++++++++++-- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs index b1d1ed8c61..e3b02e5905 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs @@ -22,8 +22,6 @@ using osu.Game.Overlays.Toolbar; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; -using osu.Game.Screens; -using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; @@ -43,9 +41,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 protected Screens.SelectV2.SongSelect SongSelect { get; private set; } = null!; protected BeatmapCarousel Carousel => SongSelect.ChildrenOfType().Single(); - [Cached] - protected readonly ScreenFooter Footer; - [Cached] private readonly OsuLogo logo; @@ -72,10 +67,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { State = { Value = Visibility.Visible }, }, - Footer = new ScreenFooter - { - BackButtonPressed = () => Stack.CurrentScreen.Exit(), - }, logo = new OsuLogo { Alpha = 0f, @@ -111,14 +102,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Add(beatmapStore); } - protected override void LoadComplete() - { - base.LoadComplete(); - - Stack.ScreenPushed += updateFooter; - Stack.ScreenExited += updateFooter; - } - public override void SetUpSteps() { base.SetUpSteps(); @@ -207,38 +190,5 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } protected void WaitForSuspension() => AddUntilStep("wait for not current", () => !SongSelect.AsNonNull().IsCurrentScreen()); - - private void updateFooter(IScreen? _, IScreen? newScreen) - { - if (newScreen is OsuScreen osuScreen && osuScreen.ShowFooter) - { - Footer.Show(); - - if (osuScreen.IsLoaded) - updateFooterButtons(); - else - { - // ensure the current buttons are immediately disabled on screen change (so they can't be pressed). - Footer.SetButtons(Array.Empty()); - - osuScreen.OnLoadComplete += _ => updateFooterButtons(); - } - - void updateFooterButtons() - { - var buttons = osuScreen.CreateFooterButtons(); - - osuScreen.LoadComponentsAgainstScreenDependencies(buttons); - - Footer.SetButtons(buttons); - Footer.Show(); - } - } - else - { - Footer.Hide(); - Footer.SetButtons(Array.Empty()); - } - } } } diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index f780b1a8f8..42199faa4d 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -7,7 +7,9 @@ using osu.Framework.Bindables; using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Logging; +using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Overlays; @@ -32,7 +34,7 @@ namespace osu.Game.Tests.Visual protected DialogOverlay DialogOverlay { get; private set; } [Cached] - private ScreenFooter footer; + protected ScreenFooter Footer { get; private set; } protected ScreenTestScene() { @@ -43,17 +45,32 @@ namespace osu.Game.Tests.Visual Name = nameof(ScreenTestScene), RelativeSizeAxes = Axes.Both }, - content = new Container { RelativeSizeAxes = Axes.Both }, + new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + content = new Container { RelativeSizeAxes = Axes.Both }, + Footer = new ScreenFooter(), + } + }, overlayContent = new Container { RelativeSizeAxes = Axes.Both, Child = DialogOverlay = new DialogOverlay() }, - footer = new ScreenFooter(), }); - Stack.ScreenPushed += (_, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}"); - Stack.ScreenExited += (_, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed ← {newScreen}"); + Stack.ScreenPushed += (oldScreen, newScreen) => + { + updateFooter(oldScreen, newScreen); + Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}"); + }; + Stack.ScreenExited += (oldScreen, newScreen) => + { + updateFooter(oldScreen, newScreen); + Logger.Log($"{nameof(ScreenTestScene)} screen changed ← {newScreen}"); + }; } protected void LoadScreen(OsuScreen screen) => Stack.Push(screen); @@ -79,6 +96,39 @@ namespace osu.Game.Tests.Visual }); } + private void updateFooter(IScreen? _, IScreen? newScreen) + { + if (newScreen is OsuScreen osuScreen && osuScreen.ShowFooter) + { + Footer.Show(); + + if (osuScreen.IsLoaded) + updateFooterButtons(); + else + { + // ensure the current buttons are immediately disabled on screen change (so they can't be pressed). + Footer.SetButtons(Array.Empty()); + + osuScreen.OnLoadComplete += _ => updateFooterButtons(); + } + + void updateFooterButtons() + { + var buttons = osuScreen.CreateFooterButtons(); + + osuScreen.LoadComponentsAgainstScreenDependencies(buttons); + + Footer.SetButtons(buttons); + Footer.Show(); + } + } + else + { + Footer.Hide(); + Footer.SetButtons(Array.Empty()); + } + } + #region IOverlayManager IBindable IOverlayManager.OverlayActivationMode { get; } = new Bindable(OverlayActivation.All); From 41b8033ebdae5249e86d8b3e5e0fd02d24563b28 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Aug 2025 21:21:47 +0900 Subject: [PATCH 57/57] Adjust interpolation workaround to catch-up slightly smoother --- osu.Android.props | 2 +- osu.Game/Beatmaps/FramedBeatmapClock.cs | 5 ++++- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 40a9b454ce..46d558354e 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 7545031cf3..3768550c21 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - +