From 065fc446da3afe7fd44388da3c6f7dcfa80fec54 Mon Sep 17 00:00:00 2001 From: Fayar35 Date: Tue, 17 Jun 2025 23:20:50 +0200 Subject: [PATCH 01/21] change StrictTrackingTailJudgement to regular TailJudgement but with LargeTickMiss in case of break --- osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index 5ee8814b5a..926700389d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -15,8 +15,10 @@ using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; +using static osu.Game.Rulesets.Osu.Objects.SliderTailCircle; namespace osu.Game.Rulesets.Osu.Mods { @@ -83,7 +85,12 @@ namespace osu.Game.Rulesets.Osu.Mods { } - public override Judgement CreateJudgement() => new OsuJudgement(); + public override Judgement CreateJudgement() => new StrictTrackingTailJudgement(); + } + + public class StrictTrackingTailJudgement : TailJudgement + { + public override HitResult MinResult => HitResult.LargeTickMiss; } private partial class StrictTrackingDrawableSliderTail : DrawableSliderTail From 48e6f09b4dd18edaa5e2749b7db4e8197e1f8f92 Mon Sep 17 00:00:00 2001 From: Fayar35 Date: Wed, 18 Jun 2025 00:15:18 +0200 Subject: [PATCH 02/21] remove using not used --- osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index 926700389d..ee4d0ae04c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -12,7 +12,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Beatmaps; -using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Scoring; From eaadd507d3d253f44674495002bc70fe3023aa7e Mon Sep 17 00:00:00 2001 From: diquoks Date: Tue, 24 Jun 2025 19:15:02 +0300 Subject: [PATCH 03/21] Add custom keybinds to tips in `Main menu` --- osu.Game/Localisation/MenuTipStrings.cs | 32 ++++++++++++------------- osu.Game/Screens/Menu/MenuTipDisplay.cs | 22 ++++++++++------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/osu.Game/Localisation/MenuTipStrings.cs b/osu.Game/Localisation/MenuTipStrings.cs index 9d398e8e64..9a52f2e279 100644 --- a/osu.Game/Localisation/MenuTipStrings.cs +++ b/osu.Game/Localisation/MenuTipStrings.cs @@ -10,14 +10,14 @@ namespace osu.Game.Localisation private const string prefix = @"osu.Game.Resources.Localisation.MenuTip"; /// - /// "Press Ctrl-T anywhere in the game to toggle the toolbar!" + /// "Press {0} anywhere in the game to toggle the toolbar!" /// - public static LocalisableString ToggleToolbarShortcut => new TranslatableString(getKey(@"toggle_toolbar_shortcut"), @"Press Ctrl-T anywhere in the game to toggle the toolbar!"); + public static LocalisableString ToggleToolbarShortcut(LocalisableString keybind) => new TranslatableString(getKey(@"toggle_toolbar_shortcut"), @"Press {0} anywhere in the game to toggle the toolbar!", keybind); /// - /// "Press Ctrl-O anywhere in the game to access settings!" + /// "Press {0} anywhere in the game to access settings!" /// - public static LocalisableString GameSettingsShortcut => new TranslatableString(getKey(@"game_settings_shortcut"), @"Press Ctrl-O anywhere in the game to access settings!"); + public static LocalisableString GameSettingsShortcut(LocalisableString keybind) => new TranslatableString(getKey(@"game_settings_shortcut"), @"Press {0} anywhere in the game to access settings!", keybind); /// /// "All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!" @@ -40,9 +40,9 @@ namespace osu.Game.Localisation public static LocalisableString ScreenScalingSettings => new TranslatableString(getKey(@"screen_scaling_settings"), @"Try adjusting the ""Screen Scaling"" mode to change your gameplay or UI area, even in fullscreen!"); /// - /// "What used to be "osu!direct" is available to all users just like on the website. You can access it anywhere using Ctrl-B!" + /// "What used to be "osu!direct" is available to all users just like on the website. You can access it anywhere using {0}!" /// - public static LocalisableString FreeOsuDirect => new TranslatableString(getKey(@"free_osu_direct"), @"What used to be ""osu!direct"" is available to all users just like on the website. You can access it anywhere using Ctrl-B!"); + public static LocalisableString FreeOsuDirect(LocalisableString keybind) => new TranslatableString(getKey(@"free_osu_direct"), @"What used to be ""osu!direct"" is available to all users just like on the website. You can access it anywhere using {0}!", keybind); /// /// "Seeking in replays is available by dragging on the progress bar at the bottom of the screen or by using the left and right arrow keys!" @@ -75,9 +75,9 @@ namespace osu.Game.Localisation public static LocalisableString ToggleAdvancedFPSCounter => new TranslatableString(getKey(@"toggle_advanced_fps_counter"), @"Toggle advanced frame / thread statistics with Ctrl-F11!"); /// - /// "You can pause during a replay by pressing Space!" + /// "You can pause during a replay by pressing {0}!" /// - public static LocalisableString ReplayPausing => new TranslatableString(getKey(@"replay_pausing"), @"You can pause during a replay by pressing Space!"); + public static LocalisableString ReplayPausing(LocalisableString keybind) => new TranslatableString(getKey(@"replay_pausing"), @"You can pause during a replay by pressing {0}!", keybind); /// /// "Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!" @@ -85,9 +85,9 @@ namespace osu.Game.Localisation public static LocalisableString ConfigurableHotkeys => new TranslatableString(getKey(@"configurable_hotkeys"), @"Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!"); /// - /// "Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via Ctrl-Shift-S!" + /// "Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via {0}!" /// - public static LocalisableString SkinEditor => new TranslatableString(getKey(@"skin_editor"), @"Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via Ctrl-Shift-S!"); + public static LocalisableString SkinEditor(LocalisableString keybind) => new TranslatableString(getKey(@"skin_editor"), @"Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via {0}!", keybind); /// /// "You can create mod presets to make toggling your favourite mod combinations easier!" @@ -100,14 +100,14 @@ namespace osu.Game.Localisation public static LocalisableString ModCustomisationSettings => new TranslatableString(getKey(@"mod_customisation_settings"), @"Many mods have customisation settings that drastically change how they function. Click the Customise button in mod select to view settings!"); /// - /// "Press Ctrl-Shift-R to switch to a random skin!" + /// "Press {0} to switch to a random skin!" /// - public static LocalisableString RandomSkinShortcut => new TranslatableString(getKey(@"random_skin_shortcut"), @"Press Ctrl-Shift-R to switch to a random skin!"); + public static LocalisableString RandomSkinShortcut(LocalisableString keybind) => new TranslatableString(getKey(@"random_skin_shortcut"), @"Press {0} to switch to a random skin!", keybind); /// - /// "While watching a replay, press Ctrl-H to toggle replay settings!" + /// "While watching a replay, press {0} to toggle replay settings!" /// - public static LocalisableString ToggleReplaySettingsShortcut => new TranslatableString(getKey(@"toggle_replay_settings_shortcut"), @"While watching a replay, press Ctrl-H to toggle replay settings!"); + public static LocalisableString ToggleReplaySettingsShortcut(LocalisableString keybind) => new TranslatableString(getKey(@"toggle_replay_settings_shortcut"), @"While watching a replay, press {0} to toggle replay settings!", keybind); /// /// "You can easily copy the mods from scores on a leaderboard by right-clicking on them!" @@ -140,9 +140,9 @@ namespace osu.Game.Localisation public static LocalisableString GlobalStatisticsShortcut => new TranslatableString(getKey(@"global_statistics_shortcut"), @"Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!"); /// - /// "When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!" + /// "When your gameplay HUD is hidden, you can press and hold {0} to view it temporarily!" /// - public static LocalisableString PeekHUDWhenHidden => new TranslatableString(getKey(@"peek_hud_when_hidden"), @"When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!"); + public static LocalisableString PeekHUDWhenHidden(LocalisableString keybind) => new TranslatableString(getKey(@"peek_hud_when_hidden"), @"When your gameplay HUD is hidden, you can press and hold {0} to view it temporarily!", keybind); /// /// "Drag and drop any image into the skin editor to load it in quickly!" diff --git a/osu.Game/Screens/Menu/MenuTipDisplay.cs b/osu.Game/Screens/Menu/MenuTipDisplay.cs index 283528d22a..f1464fcba7 100644 --- a/osu.Game/Screens/Menu/MenuTipDisplay.cs +++ b/osu.Game/Screens/Menu/MenuTipDisplay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -12,6 +13,8 @@ using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Input; +using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; using osu.Game.Localisation; @@ -27,6 +30,9 @@ namespace osu.Game.Screens.Menu private Bindable showMenuTips = null!; + [Resolved] + private RealmKeyBindingStore keyBindingStore { get; set; } = null!; + [BackgroundDependencyLoader] private void load() { @@ -101,13 +107,13 @@ namespace osu.Game.Screens.Menu { LocalisableString[] tips = { - MenuTipStrings.ToggleToolbarShortcut, - MenuTipStrings.GameSettingsShortcut, + MenuTipStrings.ToggleToolbarShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleToolbar).FirstOrDefault("Ctrl+T")), + MenuTipStrings.GameSettingsShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleSettings).FirstOrDefault("Ctrl+O")), MenuTipStrings.DynamicSettings, MenuTipStrings.NewFeaturesAreComingOnline, MenuTipStrings.UIScalingSettings, MenuTipStrings.ScreenScalingSettings, - MenuTipStrings.FreeOsuDirect, + MenuTipStrings.FreeOsuDirect(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleBeatmapListing).FirstOrDefault("Ctrl+B")), MenuTipStrings.ReplaySeeking, MenuTipStrings.MultithreadingSupport, MenuTipStrings.TryNewMods, @@ -117,15 +123,15 @@ namespace osu.Game.Screens.Menu MenuTipStrings.DiscoverPlaylists, MenuTipStrings.ToggleAdvancedFPSCounter, MenuTipStrings.GlobalStatisticsShortcut, - MenuTipStrings.ReplayPausing, + MenuTipStrings.ReplayPausing(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.TogglePauseReplay).FirstOrDefault("Space")), MenuTipStrings.ConfigurableHotkeys, - MenuTipStrings.PeekHUDWhenHidden, - MenuTipStrings.SkinEditor, + MenuTipStrings.PeekHUDWhenHidden(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.HoldForHUD).FirstOrDefault("Ctrl")), + MenuTipStrings.SkinEditor(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleSkinEditor).FirstOrDefault("Ctrl+Shift+S")), MenuTipStrings.DragAndDropImageInSkinEditor, MenuTipStrings.ModPresets, MenuTipStrings.ModCustomisationSettings, - MenuTipStrings.RandomSkinShortcut, - MenuTipStrings.ToggleReplaySettingsShortcut, + MenuTipStrings.RandomSkinShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.RandomSkin).FirstOrDefault("Ctrl+Shift+R")), + MenuTipStrings.ToggleReplaySettingsShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleReplaySettings).FirstOrDefault("Ctrl+H")), MenuTipStrings.CopyModsFromScore, MenuTipStrings.AutoplayBeatmapShortcut, MenuTipStrings.LazerIsNotAWord From 8e228e17b70a4585369240526b63ea205b04e73b Mon Sep 17 00:00:00 2001 From: diquoks Date: Wed, 25 Jun 2025 11:52:33 +0300 Subject: [PATCH 04/21] Fix default values and remove tips' array --- osu.Game/Screens/Menu/MenuTipDisplay.cs | 123 ++++++++++++++++++------ 1 file changed, 91 insertions(+), 32 deletions(-) diff --git a/osu.Game/Screens/Menu/MenuTipDisplay.cs b/osu.Game/Screens/Menu/MenuTipDisplay.cs index f1464fcba7..f0934b838d 100644 --- a/osu.Game/Screens/Menu/MenuTipDisplay.cs +++ b/osu.Game/Screens/Menu/MenuTipDisplay.cs @@ -103,41 +103,100 @@ namespace osu.Game.Screens.Menu .FadeOutFromOne(2000, Easing.OutQuint); } + private const int availableTips = 28; + private LocalisableString getRandomTip() { - LocalisableString[] tips = - { - MenuTipStrings.ToggleToolbarShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleToolbar).FirstOrDefault("Ctrl+T")), - MenuTipStrings.GameSettingsShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleSettings).FirstOrDefault("Ctrl+O")), - MenuTipStrings.DynamicSettings, - MenuTipStrings.NewFeaturesAreComingOnline, - MenuTipStrings.UIScalingSettings, - MenuTipStrings.ScreenScalingSettings, - MenuTipStrings.FreeOsuDirect(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleBeatmapListing).FirstOrDefault("Ctrl+B")), - MenuTipStrings.ReplaySeeking, - MenuTipStrings.MultithreadingSupport, - MenuTipStrings.TryNewMods, - MenuTipStrings.EmbeddedWebContent, - MenuTipStrings.BeatmapRightClick, - MenuTipStrings.TemporaryDeleteOperations, - MenuTipStrings.DiscoverPlaylists, - MenuTipStrings.ToggleAdvancedFPSCounter, - MenuTipStrings.GlobalStatisticsShortcut, - MenuTipStrings.ReplayPausing(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.TogglePauseReplay).FirstOrDefault("Space")), - MenuTipStrings.ConfigurableHotkeys, - MenuTipStrings.PeekHUDWhenHidden(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.HoldForHUD).FirstOrDefault("Ctrl")), - MenuTipStrings.SkinEditor(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleSkinEditor).FirstOrDefault("Ctrl+Shift+S")), - MenuTipStrings.DragAndDropImageInSkinEditor, - MenuTipStrings.ModPresets, - MenuTipStrings.ModCustomisationSettings, - MenuTipStrings.RandomSkinShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.RandomSkin).FirstOrDefault("Ctrl+Shift+R")), - MenuTipStrings.ToggleReplaySettingsShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleReplaySettings).FirstOrDefault("Ctrl+H")), - MenuTipStrings.CopyModsFromScore, - MenuTipStrings.AutoplayBeatmapShortcut, - MenuTipStrings.LazerIsNotAWord - }; + int tipIndex = RNG.Next(0, availableTips); - return tips[RNG.Next(0, tips.Length)]; + switch (tipIndex) + { + case 0: + return MenuTipStrings.ToggleToolbarShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleToolbar).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding); + + case 1: + return MenuTipStrings.GameSettingsShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleSettings).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding); + + case 2: + return MenuTipStrings.DynamicSettings; + + case 3: + return MenuTipStrings.NewFeaturesAreComingOnline; + + case 4: + return MenuTipStrings.UIScalingSettings; + + case 5: + return MenuTipStrings.ScreenScalingSettings; + + case 6: + return MenuTipStrings.FreeOsuDirect(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleBeatmapListing).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding); + + case 7: + return MenuTipStrings.ReplaySeeking; + + case 8: + return MenuTipStrings.MultithreadingSupport; + + case 9: + return MenuTipStrings.TryNewMods; + + case 10: + return MenuTipStrings.EmbeddedWebContent; + + case 11: + return MenuTipStrings.BeatmapRightClick; + + case 12: + return MenuTipStrings.TemporaryDeleteOperations; + + case 13: + return MenuTipStrings.DiscoverPlaylists; + + case 14: + return MenuTipStrings.ToggleAdvancedFPSCounter; + + case 15: + return MenuTipStrings.GlobalStatisticsShortcut; + + case 16: + return MenuTipStrings.ReplayPausing(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.TogglePauseReplay).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding); + + case 17: + return MenuTipStrings.ConfigurableHotkeys; + + case 18: + return MenuTipStrings.PeekHUDWhenHidden(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.HoldForHUD).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding); + + case 19: + return MenuTipStrings.SkinEditor(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleSkinEditor).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding); + + case 20: + return MenuTipStrings.DragAndDropImageInSkinEditor; + + case 21: + return MenuTipStrings.ModPresets; + + case 22: + return MenuTipStrings.ModCustomisationSettings; + + case 23: + return MenuTipStrings.RandomSkinShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.RandomSkin).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding); + + case 24: + return MenuTipStrings.ToggleReplaySettingsShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleReplaySettings).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding); + + case 25: + return MenuTipStrings.CopyModsFromScore; + + case 26: + return MenuTipStrings.AutoplayBeatmapShortcut; + + case 27: + return MenuTipStrings.LazerIsNotAWord; + } + + return string.Empty; } } } From 3923b0a949113db8a65df2876a7636f320726c77 Mon Sep 17 00:00:00 2001 From: diquoks Date: Wed, 25 Jun 2025 12:04:04 +0300 Subject: [PATCH 05/21] Rename variable --- osu.Game/Screens/Menu/MenuTipDisplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/MenuTipDisplay.cs b/osu.Game/Screens/Menu/MenuTipDisplay.cs index f0934b838d..b2c2822b49 100644 --- a/osu.Game/Screens/Menu/MenuTipDisplay.cs +++ b/osu.Game/Screens/Menu/MenuTipDisplay.cs @@ -103,11 +103,11 @@ namespace osu.Game.Screens.Menu .FadeOutFromOne(2000, Easing.OutQuint); } - private const int availableTips = 28; + private const int available_tips = 28; private LocalisableString getRandomTip() { - int tipIndex = RNG.Next(0, availableTips); + int tipIndex = RNG.Next(0, available_tips); switch (tipIndex) { From e06e74d84e579eda77a1b36de317af31dbe62df4 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 26 Jun 2025 09:58:30 +0300 Subject: [PATCH 06/21] Change `GroupDefinition` equality to be case insensitive --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 28 +++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index b09490ce32..063323af82 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -823,9 +823,31 @@ namespace osu.Game.Screens.SelectV2 /// /// Defines a grouping header for a set of carousel items. /// - /// The order of this group in the carousel, sorted using ascending order. - /// The title of this group. - public record GroupDefinition(int Order, string Title); + public record GroupDefinition + { + /// + /// The order of this group in the carousel, sorted using ascending order. + /// + public int Order { get; } + + /// + /// The title of this group. + /// + public string Title { get; } + + private readonly string uncasedTitle; + + public GroupDefinition(int order, string title) + { + Order = order; + Title = title; + uncasedTitle = title.ToLowerInvariant(); + } + + public virtual bool Equals(GroupDefinition? other) => uncasedTitle == other?.uncasedTitle; + + public override int GetHashCode() => HashCode.Combine(uncasedTitle); + } /// /// Defines a grouping header for a set of carousel items grouped by star difficulty. From b4833d80d1e292650ff5f44b908e646a7daec415 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 26 Jun 2025 09:59:12 +0300 Subject: [PATCH 07/21] Add source grouping mode --- osu.Game/Screens/Select/Filter/GroupMode.cs | 7 +++++-- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 12 ++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Filter/GroupMode.cs b/osu.Game/Screens/Select/Filter/GroupMode.cs index b3a4f36c91..04aef2fe18 100644 --- a/osu.Game/Screens/Select/Filter/GroupMode.cs +++ b/osu.Game/Screens/Select/Filter/GroupMode.cs @@ -34,6 +34,9 @@ namespace osu.Game.Screens.Select.Filter // [Description("Favourites")] // Favourites, + [Description("Last Played")] + LastPlayed, + [Description("Length")] Length, @@ -46,8 +49,8 @@ namespace osu.Game.Screens.Select.Filter [Description("Ranked Status")] RankedStatus, - [Description("Last Played")] - LastPlayed, + [Description("Source")] + Source, [Description("Title")] Title, diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index c68f377fbb..59737baab2 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -202,6 +202,9 @@ namespace osu.Game.Screens.SelectV2 return defineGroupByLength(length); }, items); + case GroupMode.Source: + return getGroupsBy(b => defineGroupBySource(b.BeatmapSet!.Metadata.Source), items); + // TODO: need implementation // // case GroupMode.Collections: @@ -225,6 +228,7 @@ namespace osu.Game.Screens.SelectV2 { return items.GroupBy(i => getGroup((BeatmapInfo)i.Model)) .OrderBy(s => s.Key.Order) + .ThenBy(s => s.Key.Title) .Select(g => new GroupMapping(g.Key, g.ToList())) .ToList(); } @@ -354,6 +358,14 @@ namespace osu.Game.Screens.SelectV2 return new GroupDefinition(11, "Over 10 minutes"); } + private GroupDefinition defineGroupBySource(string source) + { + if (string.IsNullOrEmpty(source)) + return new GroupDefinition(1, "Unsourced"); + + return new GroupDefinition(0, source); + } + private static T? aggregateMax(BeatmapInfo b, Func func) { var beatmaps = b.BeatmapSet!.Beatmaps.Where(bb => !bb.Hidden); From 7a1c7fbd7daf0c1f42bd04d77ab43544abb93719 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 26 Jun 2025 10:05:21 +0300 Subject: [PATCH 08/21] Add test coverage --- .../BeatmapCarouselFilterGroupingTest.cs | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs index 7f34d7a901..617836a5da 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -333,22 +333,32 @@ namespace osu.Game.Tests.Visual.SongSelectV2 #endregion + #region Source grouping + + [Test] + public async Task TestGroupingBySource() + { + int total = 0; + + var beatmapSets = new List(); + addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = "Cool Game", beatmapSets, out var beatmapCoolGame); + addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = "Cool game", beatmapSets, out var beatmapCoolGameB); + addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = "Nice Movie", beatmapSets, out var beatmapNiceMovie); + 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); + assertTotal(results, total); + } + + #endregion + private static async Task> runGrouping(GroupMode group, List beatmapSets) { var groupingFilter = new BeatmapCarouselFilterGrouping(() => new FilterCriteria { Group = group }); - var carouselItems = await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None); - - // sanity check to ensure no detection of two group items with equal order value. - var groups = carouselItems.Select(i => i.Model).OfType(); - - foreach (var header in groups) - { - var sameOrder = groups.FirstOrDefault(g => g != header && g.Order == header.Order); - if (sameOrder != null) - Assert.Fail($"Detected two groups with equal order number: \"{header.Title}\" vs. \"{sameOrder.Title}\""); - } - - return carouselItems; + 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) From 2016fc5ea1f3c5dd33a0a5fd1f51eeb467ec7d93 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Jul 2025 18:14:13 +0900 Subject: [PATCH 09/21] Replace `using static` with inner class name --- osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index ee4d0ae04c..129c03149f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -17,7 +17,6 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; -using static osu.Game.Rulesets.Osu.Objects.SliderTailCircle; namespace osu.Game.Rulesets.Osu.Mods { @@ -87,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override Judgement CreateJudgement() => new StrictTrackingTailJudgement(); } - public class StrictTrackingTailJudgement : TailJudgement + public class StrictTrackingTailJudgement : SliderTailCircle.TailJudgement { public override HitResult MinResult => HitResult.LargeTickMiss; } From f19bc18f494e237802d89b8f6cf230ff9cd77c92 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sat, 5 Jul 2025 12:24:36 +0300 Subject: [PATCH 10/21] Update OsuPerformanceCalculator.cs --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index a667d12a44..41b0947fbb 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate; - overallDifficulty = (80 - greatHitWindow) / 6; + overallDifficulty = (79.5 - greatHitWindow) / 6; approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5; if (osuAttributes.SliderCount > 0) From 26ede9ca592d4deb9ba1f061707914c6c2ee63d4 Mon Sep 17 00:00:00 2001 From: marvin Date: Sun, 6 Jul 2025 14:50:13 +0200 Subject: [PATCH 11/21] Add support for ruleset-select sample for custom rulesets --- .../Menus/TestSceneToolbarRulesetSelector.cs | 75 +++++++++++++++++++ .../Toolbar/ToolbarRulesetSelector.cs | 21 +++++- 2 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tests/Visual/Menus/TestSceneToolbarRulesetSelector.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarRulesetSelector.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarRulesetSelector.cs new file mode 100644 index 0000000000..6f1ecb9025 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarRulesetSelector.cs @@ -0,0 +1,75 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.IO.Stores; +using osu.Game.Beatmaps; +using osu.Game.Overlays.Toolbar; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Tests.Visual.Menus +{ + public partial class TestSceneToolbarRulesetSelector : OsuTestScene + { + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets, OsuGameBase game) + { + TestRuleset.Resources = new TestResourceStore(game.Resources); + + Dependencies.CacheAs(new TestRulesetStore(rulesets)); + + Child = new Container + { + RelativeSizeAxes = Axes.X, + Height = Toolbar.HEIGHT, + Child = new ToolbarRulesetSelector(), + }; + } + + private class TestRulesetStore : RulesetStore + { + public TestRulesetStore(RulesetStore store) + { + AvailableRulesets = store.AvailableRulesets.Append(new TestRuleset().RulesetInfo); + } + + public override IEnumerable AvailableRulesets { get; } + } + + private class TestRuleset : Ruleset + { + public static IResourceStore Resources { get; set; } = null!; + + public override IEnumerable GetModsFor(ModType type) => Enumerable.Empty(); + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) => null!; + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!; + + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!; + + public override IResourceStore CreateResourceStore() => Resources; + + public override string Description => "Test Ruleset"; + public override string ShortName => "test"; + } + + private class TestResourceStore : ResourceStore + { + public TestResourceStore(IResourceStore store) + : base(store) + { + } + + protected override IEnumerable GetFilenames(string name) => base.GetFilenames(name) + .Select(s => s.Replace("UI/ruleset-select-test", "Gameplay/failsound")); + } + } +} diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs index a979575a0b..0e2fa6688d 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.IO.Stores; using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; @@ -32,6 +33,8 @@ namespace osu.Game.Overlays.Toolbar private readonly Dictionary rulesetSelectionChannel = new Dictionary(); private Sample defaultSelectSample; + private ISampleStore samples; + public ToolbarRulesetSelector() { RelativeSizeAxes = Axes.Y; @@ -39,7 +42,7 @@ namespace osu.Game.Overlays.Toolbar } [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load(AudioManager audio, OsuGameBase game) { AddRangeInternal(new[] { @@ -66,8 +69,15 @@ namespace osu.Game.Overlays.Toolbar }, }); + var store = new ResourceStore(game.Resources); + samples = audio.GetSampleStore(new NamespacedResourceStore(store, "Samples"), audio.SampleMixer); + foreach (var r in Rulesets.AvailableRulesets) - rulesetSelectionSample[r] = audio.Samples.Get($@"UI/ruleset-select-{r.ShortName}"); + { + store.AddStore(r.CreateInstance().CreateResourceStore()); + + rulesetSelectionSample[r] = samples.Get($@"UI/ruleset-select-{r.ShortName}"); + } defaultSelectSample = audio.Samples.Get(@"UI/default-select"); @@ -159,5 +169,12 @@ namespace osu.Game.Overlays.Toolbar return false; } + + protected override void Dispose(bool isDisposing) + { + samples?.Dispose(); + + base.Dispose(isDisposing); + } } } From 8298374e598ea1d044680da094b20bf5fa5be784 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 7 Jul 2025 23:00:59 +0900 Subject: [PATCH 12/21] Fix leak from inverted event unbind --- osu.Game/Online/OnlineStatusNotifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/OnlineStatusNotifier.cs b/osu.Game/Online/OnlineStatusNotifier.cs index dda430ce6f..10d766c729 100644 --- a/osu.Game/Online/OnlineStatusNotifier.cs +++ b/osu.Game/Online/OnlineStatusNotifier.cs @@ -151,7 +151,7 @@ namespace osu.Game.Online base.Dispose(isDisposing); if (notificationsClient.IsNotNull()) - notificationsClient.MessageReceived += notifyAboutForcedDisconnection; + notificationsClient.MessageReceived -= notifyAboutForcedDisconnection; if (spectatorClient.IsNotNull()) spectatorClient.Disconnecting -= notifyAboutForcedDisconnection; From d80e4b7960731c387ada2c7c94f2cedbfeef22ea Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Jul 2025 00:22:35 +0900 Subject: [PATCH 13/21] Fix leak from no unbind from static event --- osu.Game/OsuGame.cs | 171 +++++++++++++++++++++----------------------- 1 file changed, 83 insertions(+), 88 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 57ed6a5dbf..8a4a3319e3 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -113,6 +113,9 @@ namespace osu.Game /// public const float SCREEN_EDGE_MARGIN = 12f; + private const double general_log_debounce = 60000; + private const string tablet_log_prefix = @"[Tablet] "; + public Toolbar Toolbar { get; private set; } private ChatOverlay chatOverlay; @@ -241,12 +244,26 @@ namespace osu.Game /// public virtual bool HideUnlicensedContent => false; + private bool tabletLogNotifyOnWarning = true; + private bool tabletLogNotifyOnError = true; + private int generalLogRecentCount; + public OsuGame(string[] args = null) { this.args = args; - forwardGeneralLogsToNotifications(); - forwardTabletLogsToNotifications(); + Logger.NewEntry += forwardGeneralLogToNotifications; + Logger.NewEntry += forwardTabletLogToNotifications; + + Schedule(() => + { + ITabletHandler tablet = Host.AvailableInputHandlers.OfType().SingleOrDefault(); + tablet?.Tablet.BindValueChanged(_ => + { + tabletLogNotifyOnWarning = true; + tabletLogNotifyOnError = true; + }, true); + }); } #region IOverlayManager @@ -1010,6 +1027,9 @@ namespace osu.Game base.Dispose(isDisposing); SentryLogger.Dispose(); + + Logger.NewEntry -= forwardGeneralLogToNotifications; + Logger.NewEntry -= forwardTabletLogToNotifications; } protected override IDictionary GetFrameworkConfigDefaults() @@ -1365,115 +1385,90 @@ namespace osu.Game overlay.Depth = (float)-Clock.CurrentTime; } - private void forwardGeneralLogsToNotifications() + private void forwardGeneralLogToNotifications(LogEntry entry) { - int recentLogCount = 0; + if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database || entry.Target == null) return; - const double debounce = 60000; + if (entry.Exception is SentryOnlyDiagnosticsException) + return; - Logger.NewEntry += entry => + const int short_term_display_limit = 3; + + if (generalLogRecentCount < short_term_display_limit) { - if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database || entry.Target == null) return; - - if (entry.Exception is SentryOnlyDiagnosticsException) - return; - - const int short_term_display_limit = 3; - - if (recentLogCount < short_term_display_limit) + Schedule(() => Notifications.Post(new SimpleErrorNotification { - Schedule(() => Notifications.Post(new SimpleErrorNotification - { - Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb, - Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty), - })); - } - else if (recentLogCount == short_term_display_limit) + Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb, + Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty), + })); + } + else if (generalLogRecentCount == short_term_display_limit) + { + string logFile = Logger.GetLogger(entry.Target.Value).Filename; + + Schedule(() => Notifications.Post(new SimpleNotification { - string logFile = Logger.GetLogger(entry.Target.Value).Filename; - - Schedule(() => Notifications.Post(new SimpleNotification + Icon = FontAwesome.Solid.EllipsisH, + Text = NotificationsStrings.SubsequentMessagesLogged, + Activated = () => { - Icon = FontAwesome.Solid.EllipsisH, - Text = NotificationsStrings.SubsequentMessagesLogged, - Activated = () => - { - Logger.Storage.PresentFileExternally(logFile); - return true; - } - })); - } + Logger.Storage.PresentFileExternally(logFile); + return true; + } + })); + } - Interlocked.Increment(ref recentLogCount); - Scheduler.AddDelayed(() => Interlocked.Decrement(ref recentLogCount), debounce); - }; + Interlocked.Increment(ref generalLogRecentCount); + Scheduler.AddDelayed(() => Interlocked.Decrement(ref generalLogRecentCount), general_log_debounce); } - private void forwardTabletLogsToNotifications() + private void forwardTabletLogToNotifications(LogEntry entry) { - const string tablet_prefix = @"[Tablet] "; + if (entry.Level < LogLevel.Important || entry.Target != LoggingTarget.Input || !entry.Message.StartsWith(tablet_log_prefix, StringComparison.OrdinalIgnoreCase)) + return; - bool notifyOnWarning = true; - bool notifyOnError = true; + string message = entry.Message.Replace(tablet_log_prefix, string.Empty); - Logger.NewEntry += entry => + if (entry.Level == LogLevel.Error) { - if (entry.Level < LogLevel.Important || entry.Target != LoggingTarget.Input || !entry.Message.StartsWith(tablet_prefix, StringComparison.OrdinalIgnoreCase)) + if (!tabletLogNotifyOnError) return; - string message = entry.Message.Replace(tablet_prefix, string.Empty); + tabletLogNotifyOnError = false; - if (entry.Level == LogLevel.Error) + Schedule(() => { - if (!notifyOnError) - return; - - notifyOnError = false; - - Schedule(() => + Notifications.Post(new SimpleNotification { - Notifications.Post(new SimpleNotification - { - Text = NotificationsStrings.TabletSupportDisabledDueToError(message), - Icon = FontAwesome.Solid.PenSquare, - IconColour = Colours.RedDark, - }); - - // We only have one tablet handler currently. - // The loop here is weakly guarding against a future where more than one is added. - // If this is ever the case, this logic needs adjustment as it should probably only - // disable the relevant tablet handler rather than all. - foreach (var tabletHandler in Host.AvailableInputHandlers.OfType()) - tabletHandler.Enabled.Value = false; - }); - } - else if (notifyOnWarning) - { - Schedule(() => Notifications.Post(new SimpleNotification - { - Text = NotificationsStrings.EncounteredTabletWarning, + Text = NotificationsStrings.TabletSupportDisabledDueToError(message), Icon = FontAwesome.Solid.PenSquare, - IconColour = Colours.YellowDark, - Activated = () => - { - OpenUrlExternally("https://opentabletdriver.net/Tablets", LinkWarnMode.NeverWarn); - return true; - } - })); + IconColour = Colours.RedDark, + }); - notifyOnWarning = false; - } - }; - - Schedule(() => + // We only have one tablet handler currently. + // The loop here is weakly guarding against a future where more than one is added. + // If this is ever the case, this logic needs adjustment as it should probably only + // disable the relevant tablet handler rather than all. + foreach (var tabletHandler in Host.AvailableInputHandlers.OfType()) + tabletHandler.Enabled.Value = false; + }); + } + else if (tabletLogNotifyOnWarning) { - ITabletHandler tablet = Host.AvailableInputHandlers.OfType().SingleOrDefault(); - tablet?.Tablet.BindValueChanged(_ => + Schedule(() => Notifications.Post(new SimpleNotification { - notifyOnWarning = true; - notifyOnError = true; - }, true); - }); + Text = NotificationsStrings.EncounteredTabletWarning, + Icon = FontAwesome.Solid.PenSquare, + IconColour = Colours.YellowDark, + Activated = () => + { + OpenUrlExternally("https://opentabletdriver.net/Tablets", LinkWarnMode.NeverWarn); + return true; + } + })); + + tabletLogNotifyOnWarning = false; + } } private Task asyncLoadStream; From 2f374555dc5e937d5fd6e5120007037dff1bb3f8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Jul 2025 01:54:43 +0900 Subject: [PATCH 14/21] Fix potential leak from multiple games on the same host --- osu.Game/OsuGame.cs | 58 +++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 8a4a3319e3..153e6acb3b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -364,40 +364,42 @@ namespace osu.Game if (host.Window != null) { host.Window.CursorState |= CursorState.Hidden; - host.Window.DragDrop += path => - { - // on macOS/iOS, URL associations are handled via SDL_DROPFILE events. - if (path.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) - { - HandleLink(path); - return; - } - - lock (dragDropFiles) - { - dragDropFiles.Add(path); - - Logger.Log($@"Adding ""{Path.GetFileName(path)}"" for import"); - - // File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms. - // In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch. - dragDropImportSchedule?.Cancel(); - dragDropImportSchedule = Scheduler.AddDelayed(handlePendingDragDropImports, 100); - } - }; + host.Window.DragDrop += onWindowDragDrop; } } - private void handlePendingDragDropImports() + private void onWindowDragDrop(string path) { + // on macOS/iOS, URL associations are handled via SDL_DROPFILE events. + if (path.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) + { + HandleLink(path); + return; + } + lock (dragDropFiles) { - Logger.Log($"Handling batch import of {dragDropFiles.Count} files"); + dragDropFiles.Add(path); - string[] paths = dragDropFiles.ToArray(); - dragDropFiles.Clear(); + Logger.Log($@"Adding ""{Path.GetFileName(path)}"" for import"); - Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning); + // File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms. + // In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch. + dragDropImportSchedule?.Cancel(); + dragDropImportSchedule = Scheduler.AddDelayed(handlePendingDragDropImports, 100); + } + + void handlePendingDragDropImports() + { + lock (dragDropFiles) + { + Logger.Log($"Handling batch import of {dragDropFiles.Count} files"); + + string[] paths = dragDropFiles.ToArray(); + dragDropFiles.Clear(); + + Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning); + } } } @@ -1026,8 +1028,12 @@ namespace osu.Game detachedBeatmapStore?.Dispose(); base.Dispose(isDisposing); + SentryLogger.Dispose(); + if (Host?.Window != null) + Host.Window.DragDrop -= onWindowDragDrop; + Logger.NewEntry -= forwardGeneralLogToNotifications; Logger.NewEntry -= forwardTabletLogToNotifications; } From 02cb93d854a4fdd911a07d5316e8d15c859666dd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Jul 2025 20:39:08 +0900 Subject: [PATCH 15/21] Fix leak from polling chat client being initialised too early This one is quite dumb. `OsuGame` uses [`loadComponentSingleFile`](https://github.com/ppy/osu/blob/15878f7f9fc7088494d3b66e98a7bc1004a1a06d/osu.Game/OsuGame.cs#L1228) to load the `ChannelManager`. Importantly, this process does _not_ add the component to any place in the hierarchy where it would normally be disposed - this includes `InternalChildren`, but _also_ a lesser known list of [currently-loading components](https://github.com/ppy/osu-framework/blob/cfb0d7b4b673583f0cf56273e94352769aa5bc9a/osu.Framework/Graphics/Containers/CompositeDrawable.cs#L316-L323) (those which have been sent through a `LoadComponentAsync` call). The end result of this is that, `ChannelManager` creates the `IChatClient` in its constructor, expecting to be able to dispose it, but `Dispose` is never called! And the failure case here is that `PollingChatClient` creates a background task to continuously poll the API, unfortunately keeping a reference to the rest of the world in the process. --- osu.Game/Online/Chat/ChannelManager.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index e9ca0a8ed2..fde6c4db06 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -9,6 +9,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Threading; @@ -64,7 +65,6 @@ namespace osu.Game.Online.Chat public IBindableList AvailableChannels => availableChannels; private readonly IAPIProvider api; - private readonly IChatClient chatClient; [Resolved] private UserLookupCache users { get; set; } @@ -72,6 +72,7 @@ namespace osu.Game.Online.Chat private readonly IBindable apiState = new Bindable(); private ScheduledDelegate scheduledAck; + private IChatClient chatClient = null!; private long? lastSilenceMessageId; private uint? lastSilenceId; @@ -79,14 +80,13 @@ namespace osu.Game.Online.Chat { this.api = api; - chatClient = api.GetChatClient(); - CurrentChannel.ValueChanged += currentChannelChanged; } [BackgroundDependencyLoader] private void load() { + chatClient = api.GetChatClient(); chatClient.ChannelJoined += ch => Schedule(() => joinChannel(ch)); chatClient.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false)); chatClient.NewMessages += msgs => Schedule(() => addMessages(msgs)); @@ -282,8 +282,7 @@ namespace osu.Game.Online.Chat // Check if the user has joined the requested channel already. // This uses the channel name for comparison as the PM user's username is unavailable after a restart. - var privateChannel = JoinedChannels.FirstOrDefault( - c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Name.Equals(content, StringComparison.OrdinalIgnoreCase)); + var privateChannel = JoinedChannels.FirstOrDefault(c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Name.Equals(content, StringComparison.OrdinalIgnoreCase)); if (privateChannel != null) { @@ -645,7 +644,9 @@ namespace osu.Game.Online.Chat protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - chatClient?.Dispose(); + + if (chatClient.IsNotNull()) + chatClient.Dispose(); } } From 74516a1eaa305c46cffa9cd067250d903b18ea24 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Jul 2025 20:39:21 +0900 Subject: [PATCH 16/21] Fix leak due to missing `Game` disposal --- osu.Game/Tests/Visual/OsuGameTestScene.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index b86273b4a3..fb229194d9 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -82,10 +82,11 @@ namespace osu.Game.Tests.Visual [TearDownSteps] public virtual void TearDownSteps() { - if (DebugUtils.IsNUnitRunning && Game != null) + if (DebugUtils.IsNUnitRunning) { - AddStep("exit game", () => Game.Exit()); - AddUntilStep("wait for game exit", () => Game.Parent == null); + AddStep("exit game", () => Game?.Exit()); + AddUntilStep("wait for game exit", () => Game?.Parent == null); + AddStep("dispose game", () => Game?.Dispose()); } } From cce09ceadb32c25e5c8c2dbb6f503801c60099b6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Jul 2025 22:00:44 +0900 Subject: [PATCH 17/21] Fix leaks from directly binding API bindable events --- osu.Game.Tournament/Screens/Setup/SetupScreen.cs | 5 ++++- osu.Game/Online/LocalUserStatisticsProvider.cs | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs index fed9d625ee..536e8ba767 100644 --- a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs @@ -12,6 +12,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Tournament.IPC; @@ -42,6 +43,7 @@ namespace osu.Game.Tournament.Screens.Setup [Resolved] private TournamentSceneManager? sceneManager { get; set; } + private readonly IBindable localUser = new Bindable(); private Bindable windowSize = null!; [BackgroundDependencyLoader] @@ -70,7 +72,8 @@ namespace osu.Game.Tournament.Screens.Setup }, }; - api.LocalUser.BindValueChanged(_ => Schedule(reload)); + localUser.BindTo(api.LocalUser); + localUser.BindValueChanged(_ => Schedule(reload)); stableInfo.OnStableInfoSaved += () => Schedule(reload); reload(); } diff --git a/osu.Game/Online/LocalUserStatisticsProvider.cs b/osu.Game/Online/LocalUserStatisticsProvider.cs index 22d5788c87..061f0c7e03 100644 --- a/osu.Game/Online/LocalUserStatisticsProvider.cs +++ b/osu.Game/Online/LocalUserStatisticsProvider.cs @@ -5,10 +5,12 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Users; @@ -35,6 +37,8 @@ namespace osu.Game.Online [Resolved] private IAPIProvider api { get; set; } = null!; + private readonly IBindable localUser = new Bindable(); + private readonly Dictionary statisticsCache = new Dictionary(); /// @@ -48,7 +52,8 @@ namespace osu.Game.Online { base.LoadComplete(); - api.LocalUser.BindValueChanged(_ => + localUser.BindTo(api.LocalUser); + localUser.BindValueChanged(_ => { // queuing up requests directly on user change is unsafe, as the API status may have not been updated yet. // schedule a frame to allow the API to be in its correct state sending requests. From 53df90da2c4a07470e1bdfbe5fe772c664bd6172 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Jul 2025 12:46:14 +0900 Subject: [PATCH 18/21] Update various licence years --- Directory.Build.props | 2 +- LICENCE | 2 +- Templates/osu.Game.Templates.csproj | 2 +- osu.Desktop/osu.nuspec | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 580e61dafb..a856825d87 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -50,7 +50,7 @@ https://github.com/ppy/osu Automated release. ppy Pty Ltd - Copyright (c) 2024 ppy Pty Ltd + Copyright (c) 2025 ppy Pty Ltd osu game diff --git a/LICENCE b/LICENCE index 3bb8b62d5d..9ffcc70c13 100644 --- a/LICENCE +++ b/LICENCE @@ -1,4 +1,4 @@ -Copyright (c) 2024 ppy Pty Ltd . +Copyright (c) 2025 ppy Pty Ltd . Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Templates/osu.Game.Templates.csproj b/Templates/osu.Game.Templates.csproj index 186a6093f5..ecac2e4794 100644 --- a/Templates/osu.Game.Templates.csproj +++ b/Templates/osu.Game.Templates.csproj @@ -8,7 +8,7 @@ https://github.com/ppy/osu/blob/master/Templates https://github.com/ppy/osu Automated release. - Copyright (c) 2024 ppy Pty Ltd + Copyright (c) 2025 ppy Pty Ltd Templates to use when creating a ruleset for consumption in osu!. dotnet-new;templates;osu netstandard2.1 diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec index 66b3970351..14af4d0334 100644 --- a/osu.Desktop/osu.nuspec +++ b/osu.Desktop/osu.nuspec @@ -12,7 +12,7 @@ false A free-to-win rhythm game. Rhythm is just a *click* away! testing - Copyright (c) 2024 ppy Pty Ltd + Copyright (c) 2025 ppy Pty Ltd en-AU From 80de563530932dc1125072c10bfd2fea5d44141d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Jul 2025 18:58:44 +0900 Subject: [PATCH 19/21] 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 107f54d4ac..3de0342db2 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From cb61c9e3fd7e9ca8430d5d4a1c7332fa7b19d2fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Jul 2025 18:58:46 +0900 Subject: [PATCH 20/21] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index de3fe31ee6..d071607a83 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 bb5e3da49e..c6a8c00b6c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 4f560996947bd9d787f2917aa4c3d26efa4e0e15 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 9 Jul 2025 20:30:29 +0900 Subject: [PATCH 21/21] Mark `TestSpinPerMinuteOnRewind` as flaky See: https://github.com/ppy/osu/actions/runs/16167239898/job/45631919718 --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 8d81fe3017..367a00ad3b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Storyboards; +using osu.Game.Tests; using osu.Game.Tests.Visual; using osuTK; @@ -152,6 +153,7 @@ namespace osu.Game.Rulesets.Osu.Tests } [Test] + [FlakyTest] public void TestSpinPerMinuteOnRewind() { double estimatedSpm = 0;